diff --git a/examples/signify-react-ts/package-lock.json b/examples/signify-react-ts/package-lock.json index aea27033..4195e459 100644 --- a/examples/signify-react-ts/package-lock.json +++ b/examples/signify-react-ts/package-lock.json @@ -34,7 +34,6 @@ } }, "../..": { - "name": "signify-ts", "version": "0.1.1", "license": "Apache-2.0", "dependencies": { @@ -43,6 +42,7 @@ "buffer": "^6.0.3", "cbor": "^8.0.0", "collections": "^5.1.12", + "jest-fetch-mock": "^3.0.3", "libsodium-wrappers-sumo": "^0.7.9", "lodash": "^4.17.21", "mathjs": "^11.8.2", @@ -51,9 +51,11 @@ "text-encoding": "^0.7.0", "ts-node": "^10.9.1", "urlsafe-base64": "^1.0.0", + "whatwg-fetch": "^3.6.17", "xregexp": "^5.1.0" }, "devDependencies": { + "@mermaid-js/mermaid-cli": "^10.3.0", "@size-limit/preset-small-lib": "^5.0.4", "@types/lodash": "^4.14.185", "@types/node": "^18.11.18", @@ -61,10 +63,13 @@ "@types/urlsafe-base64": "^1.0.28", "husky": "^7.0.2", "jest": "^29.3.1", + "jsdoc": "^4.0.2", + "minami": "^1.2.3", "size-limit": "^5.0.4", "ts-migrate": "^0.1.23", "tsdx": "^0.14.1", "tslib": "^2.3.1", + "typedoc": "^0.24.8", "typescript": "^4.9.4" } }, diff --git a/package-lock.json b/package-lock.json index b4b84157..0c9a5786 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "buffer": "^6.0.3", "cbor": "^8.0.0", "collections": "^5.1.12", + "jest-fetch-mock": "^3.0.3", "libsodium-wrappers-sumo": "^0.7.9", "lodash": "^4.17.21", "mathjs": "^11.8.2", @@ -22,6 +23,7 @@ "text-encoding": "^0.7.0", "ts-node": "^10.9.1", "urlsafe-base64": "^1.0.0", + "whatwg-fetch": "^3.6.17", "xregexp": "^5.1.0" }, "devDependencies": { @@ -5437,11 +5439,48 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dev": true, "dependencies": { "node-fetch": "2.6.7" } }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/cross-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/cross-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/cross-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -9562,6 +9601,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "node_modules/jest-get-type": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", @@ -12305,48 +12353,6 @@ "node": ">= 0.10.5" } }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -14000,6 +14006,11 @@ "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", "dev": true }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -20981,6 +20992,11 @@ "iconv-lite": "0.4.24" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.17", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.17.tgz", + "integrity": "sha512-c4ghIvG6th0eudYwKZY5keb81wtFz9/WeAHAoy8+r18kcWlitUIrmGFQ2rWEl4UCKUilD3zCLHOIPheHx5ypRQ==" + }, "node_modules/whatwg-mimetype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", diff --git a/package.json b/package.json index 1e1d3e6e..2696c1e4 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "buffer": "^6.0.3", "cbor": "^8.0.0", "collections": "^5.1.12", + "jest-fetch-mock": "^3.0.3", "libsodium-wrappers-sumo": "^0.7.9", "lodash": "^4.17.21", "mathjs": "^11.8.2", @@ -73,6 +74,7 @@ "text-encoding": "^0.7.0", "ts-node": "^10.9.1", "urlsafe-base64": "^1.0.0", + "whatwg-fetch": "^3.6.17", "xregexp": "^5.1.0" } } diff --git a/src/index.ts b/src/index.ts index 3b2c2665..62919ac7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,6 @@ export const ready:() => Promise = (async() => { export * from './keri/app/habery' export * from './keri/app/signify' export * from './keri/app/apping' -export * from './keri/app/client' export * from './keri/app/controller' export * from './keri/app/habery' export * from './keri/app/signify' diff --git a/src/keri/app/client.ts b/src/keri/app/client.ts deleted file mode 100644 index 54b6cbc4..00000000 --- a/src/keri/app/client.ts +++ /dev/null @@ -1,26 +0,0 @@ -export class Client { - private readonly _url: string - - constructor(url: string) { - this._url = url - } - - private url(path: string): string { - let url = new URL(path, this._url) - - return url.href - } - - getAccount(name: string): Promise { - let url = this.url(`/account/${name}`) - let req = new Request(url, {method: "GET"}) - return fetch(req) - } - - createAccount(name: string, key: string, ndig: string): Promise { - let url = this.url(`/account/${name}`) - let body = {key, ndig} - let req = new Request(url, {method: "POST", body: JSON.stringify(body)}) - return fetch(req) - } -} \ No newline at end of file diff --git a/src/keri/app/signify.ts b/src/keri/app/signify.ts index e8f369b7..7bd30b28 100644 --- a/src/keri/app/signify.ts +++ b/src/keri/app/signify.ts @@ -16,11 +16,13 @@ import { randomNonce } from "./apping" const DEFAULT_BOOT_URL = "http://localhost:3903" +/** Types of credentials */ export class CredentialTypes { static issued = "issued" static received = "received" } +/** Starte of the client */ class State { agent: any | null controller: any | null @@ -35,6 +37,7 @@ class State { } } +/** SignifyClient */ export class SignifyClient { public controller: Controller public url: string @@ -46,6 +49,13 @@ export class SignifyClient { public tier: Tier public bootUrl: string + /** + * SignifyClient constructor + * @param {string} url KERIA admin interface URL + * @param {string} bran Base64 21 char string that is used as base material for seed of the client AID + * @param {Tier} tier Security tier for generating keys of the client AID (high | mewdium | low) + * @param {string} bootUrl KERIA boot interface URL + */ constructor(url: string, bran: string, tier: Tier = Tier.low, bootUrl: string = DEFAULT_BOOT_URL) { this.url = url if (bran.length < 21) { @@ -65,6 +75,11 @@ export class SignifyClient { return [this.url, this.bran, this.pidx, this.authn] } + /** + * Boot a KERIA agent + * @async + * @returns {Promise} A promise to the result of the boot + */ async boot(): Promise{ const [evt, sign] = this.controller?.event ?? []; const data = { @@ -75,7 +90,7 @@ export class SignifyClient { tier: this.controller?.tier }; - const res = await fetch(this.bootUrl + "/boot", { + return await fetch(this.bootUrl + "/boot", { method: "POST", body: JSON.stringify(data), headers: { @@ -83,9 +98,13 @@ export class SignifyClient { } }); - return res; } + /** + * Get state of the agent and the client + * @async + * @returns {Promise} A promise to the state + */ async state(): Promise { const caid = this.controller?.pre @@ -103,6 +122,9 @@ export class SignifyClient { return state; } + /** Connect to a KERIA agent + * @async + */ async connect() { const state = await this.state() this.pidx = state.pidx @@ -121,6 +143,15 @@ export class SignifyClient { this.authn = new Authenticater(this.controller.signer, this.agent.verfer!) } + /** + * Fetch a resource from the KERIA agent + * @async + * @param {string} path Path to the resource + * @param {string} method HTTP method + * @param {any} data Data to be sent in the body of the resource + * @param {Headers} [extraHeaders] Optional extra headers to be sent with the request + * @returns {Promise} A promise to the result of the fetch + */ async fetch(path: string, method: string, data: any, extraHeaders?: Headers): Promise { let headers = new Headers() let signed_headers = new Headers() @@ -172,6 +203,16 @@ export class SignifyClient { } } + /** + * Fetch a resource from from an external URL with headers signed by an AID + * @async + * @param {string} url URL of the resource + * @param {string} path Path to the resource + * @param {string} method HTTP method + * @param {any} data Data to be sent in the body of the resource + * @param {string} aidName Name or alias of the AID to be used for signing + * @returns {Promise} A promise to the result of the fetch + */ async signedFetch(url: string, path: string, method: string, data: any, aidName: string): Promise { const hab = await this.identifiers().get(aidName) const keeper = this.manager!.get(hab) @@ -210,8 +251,13 @@ export class SignifyClient { }); } - - async approveDelegation() { + + /** + * Approve the delegation of the client AID to the KERIA agent + * @async + * @returns {Promise} A promise to the result of the approval + */ + async approveDelegation(): Promise { let sigs = this.controller.approveDelegation(this.agent!) let data = { @@ -219,7 +265,7 @@ export class SignifyClient { sigs: sigs } - await fetch(this.url + "/agent/" + this.controller.pre + "?type=ixn", { + return await fetch(this.url + "/agent/" + this.controller.pre + "?type=ixn", { method: "PUT", body: JSON.stringify(data), headers: { @@ -228,9 +274,15 @@ export class SignifyClient { }) } - async saveOldSalt(salt:string): Promise { + /** + * Save old client passcode in KERIA agent + * @async + * @param {string} passcode Passcode to be saved + * @returns {Promise} A promise to the result of the save + */ + async saveOldPasscode(passcode:string): Promise { const caid = this.controller?.pre; - const body = { salt: salt }; + const body = { salt: passcode }; return await fetch(this.url + "/salt/" + caid, { method: "PUT", body: JSON.stringify(body), @@ -240,7 +292,12 @@ export class SignifyClient { }) } - async deleteldSalt(): Promise { + /** + * Delete a saved passcode from KERIA agent + * @async + * @returns {Promise} A promise to the result of the deletion + */ + async deletePasscode(): Promise { const caid = this.controller?.pre; return await fetch(this.url + "/salt/" + caid, { method: "DELETE", @@ -250,9 +307,16 @@ export class SignifyClient { }) } - async rotate(nbran: string, aids: [string] ){ + /** + * Rotate the client AID + * @async + * @param {string} nbran Base64 21 char string that is used as base material for the new seed + * @param {Array} aids List of managed AIDs to be rotated + * @returns {Promise} A promise to the result of the rotation + */ + async rotate(nbran: string, aids: string[]): Promise{ let data = this.controller.rotate(nbran, aids) - await fetch(this.url + "/agent/" + this.controller.pre, { + return await fetch(this.url + "/agent/" + this.controller.pre, { method: "PUT", body: JSON.stringify(data), headers: { @@ -261,55 +325,104 @@ export class SignifyClient { }) } + /** + * Get identifiers resource + * @returns {Identifier} + */ identifiers(): Identifier { return new Identifier(this) } + /** + * Get OOBIs resource + * @returns {Oobis} + */ oobis(): Oobis { return new Oobis(this) } + /** + * Get operations resource + * @returns {Operations} + */ operations(): Operations { return new Operations(this) } + /** + * Get keyEvents resource + * @returns {KeyEvents} + */ keyEvents(): KeyEvents { return new KeyEvents(this) } + /** + * Get keyStates resource + * @returns {KeyStates} + */ keyStates(): KeyStates { return new KeyStates(this) } + /** + * Get credentials resource + * @returns {Credentials} + */ credentials(): Credentials { return new Credentials(this) } + /** + * Get registries resource + * @returns {Registries} + */ registries(): Registries { return new Registries(this) } + /** + * Get schemas resource + * @returns {Schemas} + */ schemas(): Schemas { return new Schemas(this) } + /** + * Get challenges resource + * @returns {Challenges} + */ challenges(): Challenges { return new Challenges(this) } + /** + * Get contacts resource + * @returns {Contacts} + */ contacts(): Contacts { return new Contacts(this) } + /** + * Get notifications resource + * @returns {Notifications} + */ notifications(): Notifications { return new Notifications(this) } + /** + * Get escrows resource + * @returns {Escrows} + */ escrows(): Escrows { return new Escrows(this) } } +/** Arguments required to create an identfier */ export interface CreateIdentiferArgs { transferable?: boolean, isith?: string | number, @@ -335,6 +448,7 @@ export interface CreateIdentiferArgs { tier?: Tier } +/** Arguments required to rotate an identfier */ export interface RotateIdentifierArgs { transferable?: boolean, nsith?: string | number, @@ -349,12 +463,22 @@ export interface RotateIdentifierArgs { rstates?: any[] } -class Identifier { +/** Identifier */ +export class Identifier { public client: SignifyClient + /** + * Identifier + * @param {SignifyClient} client + */ constructor(client: SignifyClient) { this.client = client } + /** + * List managed identifiers + * @async + * @returns {Promise} A promise to the list of managed identifiers + */ async list(): Promise { let path = `/identifiers` let data = null @@ -363,6 +487,12 @@ class Identifier { return await res.json() } + /** + * Get information for a managed identifier + * @async + * @param {string} name Name or alias of the identifier + * @returns {Promise} A promise to the identifier information + */ async get(name: string): Promise { let path = `/identifiers/${name}` let data = null @@ -371,11 +501,17 @@ class Identifier { return await res.json() } + /** + * Create a managed identifier + * @async + * @param {string} name Name or alias of the identifier + * @param {CreateIdentiferArgs} [kargs] Optional parameters to create the identifier + * @returns {Promise} A promise to the long-running operation + */ async create(name: string, kargs:CreateIdentiferArgs={}): Promise { const algo = kargs.algo == undefined ? Algos.salty : kargs.algo - let transferable = kargs.transferable ?? true let isith = kargs.isith ?? "1" let nsith = kargs.nsith ?? "1" @@ -473,15 +609,15 @@ class Identifier { this.client.pidx = this.client.pidx + 1 let res = await this.client.fetch("/identifiers", "POST", jsondata) - return res.json() + return await res.json() } /** - * interact + * Generate an interaction event in a managed identifier * @async - * @param {string} name - * @param {any} data - * @returns {Promise} + * @param {string} name Name or alias of the identifier + * @param {any} [data] Option data to be anchored in the interaction event + * @returns {Promise} A promise to the long-running operation */ async interact(name: string, data?: any): Promise { @@ -505,14 +641,14 @@ class Identifier { jsondata[keeper.algo] = keeper.params() let res = await this.client.fetch("/identifiers/" + name + "?type=ixn", "PUT", jsondata) - return res.json() + return await res.json() } /** - * rotate - * @param name - * @param {RotateIdentifierArgs} kargs + * Generate a rotation event in a managed identifier + * @param {string} name Name or alias of the identifier + * @param {RotateIdentifierArgs} [kargs] Optional parameters requiered to generate the rotation event * @returns {Promise} */ async rotate(name: string, kargs: RotateIdentifierArgs={}): Promise { @@ -534,7 +670,6 @@ class Identifier { let nsith = kargs.nsith ?? isith - // if isith is None: # compute default from newly rotated verfers above if (isith == undefined) isith = `${Math.max(1, Math.ceil(count / 2)).toString(16)}` @@ -583,17 +718,19 @@ class Identifier { jsondata[keeper.algo] = keeper.params() let res = await this.client.fetch("/identifiers/" + name, "PUT", jsondata) - return res.json() + return await res.json() } /** - * addEndRole + * Authorize an endpoint provider in a given role for a managed identifier + * @remarks + * Typically used to authorize the agent to be the endpoint provider for the identifier in the role of `agent` * @async - * @param {string} name - * @param {string} role - * @param {string} eid - * @param {string} stamp - * @returns {Promise} + * @param {string} name Name or alias of the identifier + * @param {string} role Authorized role for eid + * @param {string} [eid] Optional qb64 of endpoint provider to be authorized + * @param {string} [stamp=now] Optional date-time-stamp RFC-3339 profile of iso8601 datetime. Now is the default if not provided + * @returns {Promise} A promise to the result of the authorization */ async addEndRole(name: string, role: string, eid?: string, stamp?: string): Promise { const hab = await this.get(name) @@ -609,19 +746,19 @@ class Identifier { } let res = await this.client.fetch("/identifiers/" + name + "/endroles", "POST", jsondata) - return res.json() + return await res.json() } /** - * makeEndRole - * @param {string} pre - * @param {string} role - * @param {string} eid - * @param {string} stamp - * @returns {Serder} + * Generate an /end/role/add reply message + * @param {string} pre Prefix of the identifier + * @param {string} role Authorized role for eid + * @param {string} [eid] Optional qb64 of endpoint provider to be authorized + * @param {string} [stamp=now] Optional date-time-stamp RFC-3339 profile of iso8601 datetime. Now is the default if not provided + * @returns {Serder} The reply message */ - makeEndRole(pre: string, role: string, eid?: string, stamp?: string): Serder { + private makeEndRole(pre: string, role: string, eid?: string, stamp?: string): Serder { const data: any = { cid: pre, role: role @@ -634,31 +771,35 @@ class Identifier { } /** - * members + * Get the members of a group identifier * @async - * @param {string} name - * @returns {Promise} + * @param {string} name - Name or alias of the identifier + * @returns {Promise} - A promise to the list of members */ async members(name: string): Promise { let res = await this.client.fetch("/identifiers/" + name + "/members", "GET", undefined) - return res.json() + return await res.json() } } /** * Oobis */ -class Oobis { +export class Oobis { public client: SignifyClient + /** + * Oobis + * @param {SignifyClient} client + */ constructor(client: SignifyClient) { this.client = client } /** - * get - * @param {string} name - * @param {string} role - * @returns {Promise} + * Get the OOBI(s) for a managed indentifier for a given role + * @param {string} name Name or alias of the identifier + * @param {string} role Authorized role + * @returns {Promise} A promise to the OOBI(s) */ async get(name: string, role: string = 'agent'): Promise { let path = `/identifiers/${name}/oobis?role=${role}` @@ -669,11 +810,11 @@ class Oobis { } /** - * resolve + * Resolve an OOBI * @async - * @param oobi - * @param alias - * @returns {Promise} + * @param {string} oobi The OOBI to be resolver + * @param {string} [alias] Optional name or alias to link the OOBI resolution to a contact + * @returns {Promise} A promise to the long-running operation */ async resolve(oobi: string, alias?: string): Promise { let path = `/oobis` @@ -692,18 +833,24 @@ class Oobis { /** * Operations + * @remarks + * Operations represent the status and result of long running tasks performed by KERIA agent */ -class Operations { +export class Operations { public client: SignifyClient + /** + * Operations + * @param {SignifyClient} client + */ constructor(client: SignifyClient) { this.client = client } /** - * get + * Get operation status * @async - * @param {string} name - * @returns {Promise} + * @param {string} name Name of the operation + * @returns {Promise} A promise to the status of the operation */ async get(name: string): Promise { let path = `/operations/${name}` @@ -718,17 +865,21 @@ class Operations { /** * KeyEvents */ -class KeyEvents { +export class KeyEvents { public client: SignifyClient + /** + * KeyEvents + * @param {SignifyClient} client + */ constructor(client: SignifyClient) { this.client = client } /** - * get + * Retrieve key events for an identifier * @async - * @param {string} pre - * @returns {Promise} + * @param {string} pre Identifier prefix + * @returns {Promise} A promise to the key events */ async get(pre: string): Promise { let path = `/events?pre=${pre}` @@ -743,17 +894,21 @@ class KeyEvents { /** * KeyStates */ -class KeyStates { +export class KeyStates { public client: SignifyClient + /** + * KeyStates + * @param {SignifyClient} client + */ constructor(client: SignifyClient) { this.client = client } /** - * get + * Retriene the key state for an identifier * @async - * @param {string} pre - * @returns {Promise} + * @param {string} pre Identifier prefix + * @returns {Promise} A promise to the key states */ async get(pre: string): Promise { let path = `/states?pre=${pre}` @@ -765,10 +920,10 @@ class KeyStates { } /** - * list + * Retrieve the key state for a list of identifiers * @async - * @param {Array} pres - * @returns {Promise} + * @param {Array} pres List of identifier prefixes + * @returns {Promise} A promise to the key states */ async list(pres: [string]): Promise { let path = `/states?${pres.map(pre => `pre=${pre}`).join('&')}` @@ -780,12 +935,12 @@ class KeyStates { } /** - * query + * Query the key state of an identifier for a given sequence number or anchor SAID * @async - * @param {string} pre - * @param {number} sn - * @param {string} anchor - * @returns {Promise} + * @param {string} pre Identifier prefix + * @param {number} [sn] Optional sequence number + * @param {string} [anchor] Optional anchor SAID + * @returns {Promise} A promise to the long-running operation */ async query(pre: string, sn?: number, anchor?: string): Promise { let path = `/queries` @@ -805,6 +960,7 @@ class KeyStates { } } +/** Credential filter parameters */ export interface CredentialFilter { filter?: object, sort?: object[], @@ -815,18 +971,22 @@ export interface CredentialFilter { /** * Credentials */ -class Credentials { +export class Credentials { public client: SignifyClient + /** + * Credentials + * @param {SignifyClient} client + */ constructor(client: SignifyClient) { this.client = client } /** - * list + * List credentials * @async - * @param {string} name - * @param {CredentialFilter} kargs - * @returns + * @param {string} name Name or alias of the identifier + * @param {CredentialFilter} [kargs] Optional parameters to filter the credentials + * @returns {Promise} A promise to the list of credentials */ async list(name: string, kargs:CredentialFilter ={}): Promise { let path = `/identifiers/${name}/credentials/query` @@ -848,12 +1008,12 @@ class Credentials { } /** - * get + * Get a credential * @async - * @param {string} name - * @param {string} said - * @param {boolean} includeCESR - * @returns {Promise} + * @param {string} name - Name or alias of the identifier + * @param {string} said - SAID of the credential + * @param {boolean} [includeCESR=false] - Optional flag export the credential in CESR format + * @returns {Promise} A promise to the credential */ async get(name: string, said: string, includeCESR: boolean = false): Promise { let path = `/identifiers/${name}/credentials/${said}` @@ -865,20 +1025,19 @@ class Credentials { } /** - * issue + * Issue a credential * @async - * @param {string} name - * @param {string} registy - * @param {string} schema - * @param {string} recipient - * @param {any} credentialData - * @param {any} rules - * @param {any} source - * @param {boolean} priv - * @returns {Promise} + * @param {string} name Name or alias of the identifier + * @param {string} registy qb64 AID of credential registry + * @param {string} schema SAID of the schema + * @param {string} [recipient] Optional prefix of recipient identifier + * @param {any} [credentialData] Optional credential data + * @param {any} [rules] Optional credential rules + * @param {any} [source] Optional credential sources + * @param {boolean} [priv=false] Flag to issue a credential with privacy preserving features + * @returns {Promise} A promise to the long-running operation */ async issue(name: string, registy: string, schema: string, recipient?: string, credentialData?: any, rules?: any, source?: any, priv: boolean=false): Promise { - // Create Credential let hab = await this.client.identifiers().get(name) let pre: string = hab.prefix @@ -988,11 +1147,11 @@ class Credentials { } /** - * revoke + * Revoke credential * @async - * @param {string} name - * @param {string} said - * @returns {Promise} + * @param {string} name Name or alias of the identifier + * @param {string} said SAID of the credential + * @returns {Promise} A promise to the long-running operation */ async revoke(name: string, said: string): Promise { let hab = await this.client.identifiers().get(name) @@ -1066,13 +1225,13 @@ class Credentials { } /** - * present + * Present a credential * @async - * @param {string} name - * @param {string} said - * @param {string} recipient - * @param {boolean} include - * @returns {Promise} + * @param {string} name Name or alias of the identifier + * @param {string} said SAID of the credential + * @param {string} recipient Identifier prefix of the receiver of the presentation + * @param {boolean} [include=true] Flag to indicate whether to stream credential alongside presentation exchange message + * @returns {Promise} A promise to the long-running operation */ async present(name: string, said: string, recipient: string, include: boolean=true): Promise { @@ -1129,13 +1288,13 @@ class Credentials { } /** - * request + * Request a presentation of a credential * @async - * @param {string} name - * @param {string} recipient - * @param {string} schema - * @param {string} issuer - * @returns {Promise} + * @param {string} name Name or alias of the identifier + * @param {string} recipient Identifier prefix of the receiver of the presentation + * @param {string} schema SAID of the schema + * @param {string} [issuer] Optional prefix of the issuer of the credential + * @returns {Promise} A promise to the long-running operation */ async request(name: string, recipient: string, schema: string, issuer?: string): Promise { let hab = await this.client.identifiers().get(name) @@ -1193,17 +1352,21 @@ class Credentials { /** * Registries */ -class Registries { +export class Registries { public client: SignifyClient + /** + * Registries + * @param {SignifyClient} client + */ constructor(client: SignifyClient) { this.client = client } /** - * list + * List registries * @async - * @param {string} name - * @returns {Promise} + * @param {string} name Name or alias of the identifier + * @returns {Promise} A promise to the list of registries */ async list(name:string): Promise { let path = `/identifiers/${name}/registries` @@ -1214,12 +1377,12 @@ class Registries { } /** - * create + * Create a registry * @async - * @param {string} name - * @param {string} registryName - * @param {string} nonce - * @returns {Promise} + * @param {string} name Name or alias of the identifier + * @param {string} registryName Name for the registry + * @param {string} [nonce] Nonce used to generate the registry. If not provided a random nonce will be generated + * @returns {Promise} A promise to the long-running operation */ async create(name: string, registryName: string, nonce?:string): Promise { // TODO add backers option @@ -1296,17 +1459,21 @@ class Registries { /** * Schemas */ -class Schemas { +export class Schemas { client: SignifyClient + /** + * Schemas + * @param {SignifyClient} client + */ constructor(client: SignifyClient) { this.client = client } /** - * get + * Get a schema * @async - * @param {string} said - * @returns {Promise} + * @param {string} said SAID of the schema + * @returns {Promise} A promise to the schema */ async get(said: string): Promise { let path = `/schema/${said}` @@ -1316,9 +1483,9 @@ class Schemas { } /** - * list + * List schemas * @async - * @returns {Promise} + * @returns {Promise} A promise to the list of schemas */ async list(): Promise { let path = `/schema` @@ -1331,9 +1498,8 @@ class Schemas { /** * Challenges */ -class Challenges { +export class Challenges { client: SignifyClient - /** * Challenges * @param {SignifyClient} client @@ -1343,12 +1509,12 @@ class Challenges { } /** - * generate + * Generate a random challenge word list based on BIP39 * @async - * @param {number} strength - * @returns {Promise} + * @param {number} strength Integer representing the strength of the challenge. Typically 128 or 256 + * @returns {Promise} A promise to the list of random words */ - async generate(strength: number = 128): Promise { + async generate(strength: number = 128): Promise { let path = `/challenges?strength=${strength.toString()}` let method = 'GET' let res = await this.client.fetch(path, method, null) @@ -1356,14 +1522,14 @@ class Challenges { } /** - * respond + * Respond to a challenge by signing a message with the list of words * @async - * @param {string} name - * @param {string} recipient - * @param {Array} words - * @returns {any} + * @param {string} name Name or alias of the identifier + * @param {string} recipient Prefix of the recipient of the response + * @param {Array} words List of words to embed in the signed response + * @returns {Promise} A promise to the result of the response */ - async respond(name: string, recipient: string, words: string[]) { + async respond(name: string, recipient: string, words: string[]): Promise { let path = `/challenges/${name}` let method = 'POST' @@ -1404,21 +1570,15 @@ class Challenges { sig: new TextDecoder().decode(ims) } - let resp = await this.client.fetch(path, method, jsondata) - if (resp.status === 202) { - return exn.ked.d - } - else { - return resp - } + return await this.client.fetch(path, method, jsondata) } /** - * accept - * @param {string} name - * @param {string} pre - * @param {string} said - * @returns {Promise} + * Accept a challenge response as valid (list of words are correct) + * @param {string} name Name or alias of the identifier + * @param {string} pre Prefix of the identifier that was challenged + * @param {string} said SAID of the challenge response message + * @returns {Promise} A promise to the result of the response */ async accept(name: string, pre: string, said: string): Promise { let path = `/challenges/${name}` @@ -1436,9 +1596,8 @@ class Challenges { /** * Contacts */ -class Contacts { +export class Contacts { client: SignifyClient - /** * Contacts * @param {SignifyClient} client @@ -1448,12 +1607,12 @@ class Contacts { } /** - * list + * List contacts * @async - * @param {string} group - * @param {string} filterField - * @param {string} filterValue - * @returns {Promise} + * @param {string} [group] Optional group name to filter contacts + * @param {string} [filterField] Optional field name to filter contacts + * @param {string} [filterValue] Optional field value to filter contacts + * @returns {Promise} A promise to the list of contacts */ async list(group?:string, filterField?:string, filterValue?:string): Promise { let params = new URLSearchParams() @@ -1468,10 +1627,10 @@ class Contacts { } /** - * get + * Get a contact * @async - * @param {string} pre - * @returns {Promise} + * @param {string} pre Prefix of the contact + * @returns {Promise} A promise to the contact */ async get(pre:string): Promise { @@ -1483,11 +1642,11 @@ class Contacts { } /** - * add + * Add a contact * @async - * @param {string} pre - * @param {any} info - * @returns {Promise} + * @param {string} pre Prefix of the contact + * @param {any} info Information about the contact + * @returns {Promise} A promise to the result of the addition */ async add(pre: string, info: any): Promise { let path = `/contacts/`+ pre @@ -1498,10 +1657,10 @@ class Contacts { } /** - * delete + * Delete a contact * @async - * @param {string} pre - * @returns {Promise} + * @param {string} pre Prefix of the contact + * @returns {Promise} A promise to the result of the deletion */ async delete(pre: string): Promise { let path = `/contacts/`+ pre @@ -1512,11 +1671,11 @@ class Contacts { } /** - * update + * Update a contact * @async - * @param {string} pre - * @param {any} info - * @returns {Promise} + * @param {string} pre Prefix of the contact + * @param {any} info Updated information about the contact + * @returns {Promise} A promise to the result of the update */ async update(pre: string, info: any): Promise { let path = `/contacts/` + pre @@ -1531,7 +1690,7 @@ class Contacts { /** * Notifications */ -class Notifications { +export class Notifications { client: SignifyClient /** @@ -1543,11 +1702,11 @@ class Notifications { } /** - * list + * List notifications * @async - * @param {string} last - * @param {number} limit - * @returns {Promise} + * @param {string} last SAID of the last notification received + * @param {number} limit number of notifications to return + * @returns {Promise} A promise to the list of notifications */ async list(last?:string, limit?:number): Promise { let params = new URLSearchParams() @@ -1561,13 +1720,12 @@ class Notifications { } /** - * mark + * Mark a notification as read * @async - * @param {string} said - * @returns {Promise} + * @param {string} said SAID of the notification + * @returns {Promise} A promise to the result of the marking */ async mark(said:string): Promise { - let path = `/notifications/`+ said let method = 'PUT' let res = await this.client.fetch(path, method, null) @@ -1575,13 +1733,12 @@ class Notifications { } /** - * delete + * Delete a notification * @async - * @param {string} said - * @returns {Promise} + * @param {string} said SAID of the notification + * @returns {Promise} A promise to the result of the deletion */ async delete(said:string) { - let path = `/notifications/`+ said let method = 'DELETE' let res = await this.client.fetch(path, method, null) @@ -1593,7 +1750,7 @@ class Notifications { /** * Escrows */ -class Escrows { +export class Escrows { client: SignifyClient /** @@ -1605,18 +1762,18 @@ class Escrows { } /** - * listReply + * List replay messages * @async - * @param {string} route - * @returns {Promise} + * @param {string} [route] Optional route in the replay message + * @returns {Promise} A promise to the list of replay messages */ - async listReply(route?:string) { + async listReply(route?:string): Promise { let params = new URLSearchParams() if (route !== undefined) {params.append('route', route)} let path = `/escrows/rpy` + '?' + params.toString() let method = 'GET' - let res = await this.client.fetch(path, method, null) + let res = await this.client.fetch(path, method, null) return await res.json() } } diff --git a/src/keri/core/keeping.ts b/src/keri/core/keeping.ts index 385e4736..78cefe83 100644 --- a/src/keri/core/keeping.ts +++ b/src/keri/core/keeping.ts @@ -241,8 +241,6 @@ export class RandyKeeper { public algo:Algos = Algos.randy public signers:Signer[] - - constructor(salter:Salter, code=MtrDex.Ed25519_Seed, count=1, icodes:string[]|undefined=undefined, transferable=false, ncode=MtrDex.Ed25519_Seed, ncount=1, ncodes:string[], dcode=MtrDex.Blake3_256, prxs:string[]|undefined=undefined, nxts:string[]|undefined=undefined){ diff --git a/test/app/signify.test.ts b/test/app/signify.test.ts new file mode 100644 index 00000000..b49ff6c9 --- /dev/null +++ b/test/app/signify.test.ts @@ -0,0 +1,391 @@ +import {strict as assert} from "assert" +import { SignifyClient, Identifier, Operations, KeyEvents, KeyStates, Contacts, Notifications, Credentials, Registries, Schemas, Challenges, Escrows, Oobis} from "../../src/keri/app/signify" +import { Authenticater } from "../../src/keri/core/authing" +import { Salter, Tier } from "../../src/keri/core/salter" +import { Algos } from "../../src/keri/core/manager" +import libsodium from "libsodium-wrappers-sumo" +import fetchMock from "jest-fetch-mock" +import 'whatwg-fetch' + +fetchMock.enableMocks(); + +const url = "http://127.0.0.1:3901" +const boot_url = "http://127.0.0.1:3903" +const bran = "0123456789abcdefghijk" + +const mockConnect = '{"agent":{"vn":[1,0],"i":"EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei",'+ + '"s":"0","p":"","d":"EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei","f":"0",'+ + '"dt":"2023-08-19T21:04:57.948863+00:00","et":"dip","kt":"1",'+ + '"k":["DMZh_y-H5C3cSbZZST-fqnsmdNTReZxIh0t2xSTOJQ8a"],"nt":"1",'+ + '"n":["EM9M2EQNCBK0MyAhVYBvR98Q0tefpvHgE-lHLs82XgqC"],"bt":"0","b":[],'+ + '"c":[],"ee":{"s":"0","d":"EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei","br":[],"ba":[]},'+ + '"di":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose"},"controller":{"state":{"vn":[1,0],'+ + '"i":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose","s":"0","p":"",'+ + '"d":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose","f":"0","dt":"2023-08-19T21:04:57.959047+00:00",'+ + '"et":"icp","kt":"1","k":["DAbWjobbaLqRB94KiAutAHb_qzPpOHm3LURA_ksxetVc"],"nt":"1",'+ + '"n":["EIFG_uqfr1yN560LoHYHfvPAhxQ5sN6xZZT_E3h7d2tL"],"bt":"0","b":[],"c":[],"ee":{"s":"0",'+ + '"d":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose","br":[],"ba":[]},"di":""},'+ + '"ee":{"v":"KERI10JSON00012b_","t":"icp","d":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose",'+ + '"i":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose","s":"0","kt":"1",'+ + '"k":["DAbWjobbaLqRB94KiAutAHb_qzPpOHm3LURA_ksxetVc"],"nt":"1",'+ + '"n":["EIFG_uqfr1yN560LoHYHfvPAhxQ5sN6xZZT_E3h7d2tL"],"bt":"0","b":[],"c":[],"a":[]}},"ridx":0,'+ + '"pidx":0}' +const mockGetAID ={ + "name": "aid1", + "prefix": "ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK", + "salty": { + "sxlt": "1AAHnNQTkD0yxOC9tSz_ukbB2e-qhDTStH18uCsi5PCwOyXLONDR3MeKwWv_AVJKGKGi6xiBQH25_R1RXLS2OuK3TN3ovoUKH7-A", + "pidx": 0, + "kidx": 0, + "stem": "signify:aid", + "tier": "low", + "dcode": "E", + "icodes": [ + "A" + ], + "ncodes": [ + "A" + ], + "transferable": true + }, + "transferable": true, + "state": { + "vn": [ + 1, + 0 + ], + "i": "ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK", + "s": "0", + "p": "", + "d": "ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK", + "f": "0", + "dt": "2023-08-21T22:30:46.473545+00:00", + "et": "icp", + "kt": "1", + "k": [ + "DPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9" + ], + "nt": "1", + "n": [ + "EAORnRtObOgNiOlMolji-KijC_isa3lRDpHCsol79cOc" + ], + "bt": "0", + "b": [], + "c": [], + "ee": { + "s": "0", + "d": "ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK", + "br": [], + "ba": [] + }, + "di": "" + }, + "windexes": [] +} + + +fetchMock.mockResponse(req => { + if (req.url.startsWith(url+'/agent')) { + return Promise.resolve({body: mockConnect, init:{ status: 202 }}) + } else if (req.url == boot_url+'/boot') { + return Promise.resolve({body: "", init:{ status: 202 }}) + } else { + let headers = new Headers() + let signed_headers = new Headers() + + headers.set('Signify-Resource', "EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei") + headers.set('Signify-Timestamp', new Date().toISOString().replace('Z', '000+00:00')) + headers.set('Content-Type', 'application/json') + + const url = new URL(req.url) + let salter = new Salter({qb64: '0AAwMTIzNDU2Nzg5YWJjZGVm'}) + let signer = salter.signer("A",true,"agentagent-ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose00",Tier.low) + + let authn = new Authenticater(signer!,signer!.verfer) + signed_headers = authn.sign(headers, req.method, url.pathname.split('?')[0]) + + return Promise.resolve({body: JSON.stringify(mockGetAID), init:{ status: 202, headers:signed_headers }}) + } + +}) + +describe('SignifyClient', () => { + it('SignifyClient initialization', async () => { + await libsodium.ready; + + let t = () => {new SignifyClient(url, 'short', Tier.low, boot_url)} + expect(t).toThrow('bran must be 21 characters') + + let client = new SignifyClient(url, bran, Tier.low, boot_url) + assert.equal(client.bran, "0123456789abcdefghijk") + assert.equal(client.url, url) + assert.equal(client.bootUrl, boot_url) + assert.equal(client.tier, Tier.low) + assert.equal(client.pidx, 0) + assert.equal(client.controller.pre, "ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose") + assert.equal(client.controller.stem, "signify:controller") + assert.equal(client.controller.tier, Tier.low) + assert.equal(client.controller.serder.raw, '{"v":"KERI10JSON00012b_","t":"icp",'+ + '"d":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose",' + + '"i":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose","s":"0",' + + '"kt":"1","k":["DAbWjobbaLqRB94KiAutAHb_qzPpOHm3LURA_ksxetVc"],' + + '"nt":"1","n":["EIFG_uqfr1yN560LoHYHfvPAhxQ5sN6xZZT_E3h7d2tL"],' + + '"bt":"0","b":[],"c":[],"a":[]}') + assert.deepEqual(client.controller.serder.ked.s, "0") + + let res = await client.boot() + assert.equal(fetchMock.mock.calls[0]![0]!,boot_url+'/boot') + assert.equal(fetchMock.mock.calls[0]![1]!.body!.toString(),'{"icp":{"v":"KERI10JSON00012b_","t":"icp","d":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose","i":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose","s":"0","kt":"1","k":["DAbWjobbaLqRB94KiAutAHb_qzPpOHm3LURA_ksxetVc"],"nt":"1","n":["EIFG_uqfr1yN560LoHYHfvPAhxQ5sN6xZZT_E3h7d2tL"],"bt":"0","b":[],"c":[],"a":[]},"sig":"AACJwsJ0mvb4VgxD87H4jIsiT1QtlzznUy9zrX3lGdd48jjQRTv8FxlJ8ClDsGtkvK4Eekg5p-oPYiPvK_1eTXEG","stem":"signify:controller","pidx":1,"tier":"low"}') + assert.equal(res.status, 202) + + await client.connect() + + // validate agent + assert(client.agent!.pre, 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei') + assert(client.agent!.anchor, 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose') + assert(client.agent!.said, 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei') + assert(client.agent!.state.s,'0') + assert(client.agent!.state.d,'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei') + + // validate approve delegation + assert.equal(client.controller.serder.ked.s, "1") + assert.equal(client.controller.serder.ked.t, "ixn") + assert.equal(client.controller.serder.ked.a[0].i, "EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei") + assert.equal(client.controller.serder.ked.a[0].d, "EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei") + assert.equal(client.controller.serder.ked.a[0].s, "0") + + let data =client.data + assert(data[0],url) + assert(data[0],bran) + + assert.equal(client.identifiers() instanceof Identifier, true) + assert.equal(client.operations() instanceof Operations, true) + assert.equal(client.keyEvents() instanceof KeyEvents, true) + assert.equal(client.keyStates() instanceof KeyStates, true) + assert.equal(client.keyStates() instanceof KeyStates, true) + assert.equal(client.credentials() instanceof Credentials, true) + assert.equal(client.registries() instanceof Registries, true) + assert.equal(client.schemas() instanceof Schemas, true) + assert.equal(client.challenges() instanceof Challenges, true) + assert.equal(client.contacts() instanceof Contacts, true) + assert.equal(client.notifications() instanceof Notifications, true) + assert.equal(client.escrows() instanceof Escrows, true) + assert.equal(client.oobis() instanceof Oobis, true) + + }) + + it('Signed fetch', async () => { + await libsodium.ready + await libsodium.ready; + const bran = "0123456789abcdefghijk" + const client = new SignifyClient(url, bran, Tier.low, boot_url) + + await client.connect() + + let resp = await client.fetch('/contacts','GET', undefined) + assert.equal(resp.status, 202) + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + assert.equal(lastCall[0]!,url+'/contacts') + assert.equal(lastCall[1]!.method,'GET') + let lastHeaders = new Headers((lastCall[1]!.headers!)) + assert.equal(lastHeaders.get('signify-resource'),'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose') + + // Headers in error + let badAgentHeaders = { + 'signify-resource': 'bad_resource', + 'signify-timestamp': '2023-08-20T15:34:31.534673+00:00', + 'signature-input': 'signify=("signify-resource" "@method" "@path" "signify-timestamp");created=1692545671;keyid="EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei";alg="ed25519"', + 'signature': 'indexed="?0";signify="0BDiSoxCv42h2BtGMHy_tpWAqyCgEoFwRa8bQy20mBB2D5Vik4gRp3XwkEHtqy6iy6SUYAytMUDtRbewotAfkCgN"', + 'content-type': 'application/json', + } + fetchMock.mockResponseOnce('[]', { status: 202, headers: badAgentHeaders }) + let t = async () => await client.fetch('/contacts','GET', undefined) + expect(t).rejects.toThrowError('message from a different remote agent') + + badAgentHeaders = { + 'signify-resource': 'EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei', + 'signify-timestamp': '2023-08-20T15:34:31.534673+00:00', + 'signature-input': 'signify=("signify-resource" "@method" "@path" "signify-timestamp");created=1692545671;keyid="EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei";alg="ed25519"', + 'signature': 'indexed="?0";signify="0BDiSoxCv42h2BtGMHy_tpWAqyCgEoFwRa8bQy20mBB2D5Vik4gRp3XwkEHtqy6iy6SUYAytMUDtRbewotAfkCbad"', + 'content-type': 'application/json' + } + fetchMock.mockResponseOnce('[]', { status: 202, headers: badAgentHeaders }) + t = async () => await client.fetch('/contacts','GET', undefined) + expect(t).rejects.toThrowError('Signature for EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei invalid.') + + + // Other calls + resp = await client.saveOldPasscode('1234') + assert.equal(resp.status, 202) + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + assert.equal(lastCall[0]!,url+'/salt/ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose') + assert.equal(lastCall[1]!.method,'PUT') + assert.equal(lastCall[1]!.body,'{"salt":"1234"}') + + resp = await client.deletePasscode() + assert.equal(resp.status, 202) + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + assert.equal(lastCall[0]!,url+'/salt/ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose') + assert.equal(lastCall[1]!.method,'DELETE') + + resp = await client.rotate("abcdefghijk0123456789",[]) + assert.equal(resp.status, 202) + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + assert.equal(lastCall[0]!,url+'/agent/ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose') + assert.equal(lastCall[1]!.method,'PUT') + let lastBody = JSON.parse(lastCall[1]!.body!) + assert.equal(lastBody.rot.t,'rot') + assert.equal(lastBody.rot.s,'1') + assert.deepEqual(lastBody.rot.kt,['1','0']) + assert.equal(lastBody.rot.d,'EGFi9pCcRaLK8dPh5S7JP9Em62fBMiR1l4gW1ZazuuAO') + + }) + + it('Salty identifiers', async () => { + await libsodium.ready; + const bran = "0123456789abcdefghijk" + + let client = new SignifyClient(url, bran, Tier.low, boot_url) + + await client.boot() + await client.connect() + + let identifiers = client.identifiers() + + await identifiers.list() + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + assert.equal(lastCall[0]!,url+'/identifiers') + assert.equal(lastCall[1]!.method,'GET') + + await client.identifiers().create('aid1', {bran: '0123456789abcdefghijk'}) + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + let lastBody = JSON.parse(lastCall[1]!.body!.toString()) + assert.equal(lastCall[0]!,url+'/identifiers') + assert.equal(lastCall[1]!.method,'POST') + assert.equal(lastBody.name,'aid1') + assert.deepEqual(lastBody.icp,{"v":"KERI10JSON00012b_","t":"icp","d":"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK","i":"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK","s":"0","kt":"1","k":["DPmhSfdhCPxr3EqjxzEtF8TVy0YX7ATo0Uc8oo2cnmY9"],"nt":"1","n":["EAORnRtObOgNiOlMolji-KijC_isa3lRDpHCsol79cOc"],"bt":"0","b":[],"c":[],"a":[]}) + assert.deepEqual(lastBody.sigs,["AACZZe75PvUZ1lCREPxFAcX59XHo-BGMYTAGni-I4E0eqKznrEoK2d-mtWmWHwKns7tfnjOzTfDUcv7PLFJ52g0A"]) + assert.deepEqual(lastBody.salty.pidx,0) + assert.deepEqual(lastBody.salty.kidx,0) + assert.deepEqual(lastBody.salty.stem,"signify:aid") + assert.deepEqual(lastBody.salty.tier,"low") + assert.deepEqual(lastBody.salty.icodes,["A"]) + assert.deepEqual(lastBody.salty.ncodes,["A"]) + assert.deepEqual(lastBody.salty.dcode,"E") + assert.deepEqual(lastBody.salty.transferable,true) + + await client.identifiers().create('aid2', {count:3, ncount:3, isith:"2", nsith:"2", bran:"0123456789lmnopqrstuv"}) + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + lastBody = JSON.parse(lastCall[1]!.body!.toString()) + assert.equal(lastCall[0]!,url+'/identifiers') + assert.equal(lastCall[1]!.method,'POST') + assert.equal(lastBody.name,'aid2') + assert.deepEqual(lastBody.icp,{"v":"KERI10JSON0001e7_","t":"icp","d":"EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX","i":"EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX","s":"0","kt":"2","k":["DGBw7C7AfC7jbD3jLLRS3SzIWFndM947TyNWKQ52iQx5","DD_bHYFsgWXuCbz3SD0HjCIe_ITjRvEoCGuZ4PcNFFDz","DEe9u8k0fm1wMFAuOIsCtCNrpduoaV5R21rAcJl0awze"],"nt":"2","n":["EML5FrjCpz8SEl4dh0U15l8bMRhV_O5iDcR1opLJGBSH","EJpKquuibYTqpwMDqEFAFs0gwq0PASAHZ_iDmSF3I2Vg","ELplTAiEKdobFhlf-dh1vUb2iVDW0dYOSzs1dR7fQo60"],"bt":"0","b":[],"c":[],"a":[]}) + assert.deepEqual(lastBody.sigs,["AAD9_IgPaUEBjAl1Ck61Jkn78ErzsnVkIxpaFBYSdSEAW4NbtXsLiUn1olijzdTQYn_Byq6MaEk-eoMN3Oc0WEEC","ABBWJ7KkAXXiRK8JyEUpeARHJTTzlBHu_ev-jUrNEhV9sX4_4lI7wxowrQisumt5r50bUNfYBK7pxSwHk8I4IFQP","ACDTITaEquHdYKkS-94tVCxL3IYrtvhlTt__sSUavTJT6fI3KB-uwXV7L0SfzMq0gFqYxkheH2LdC4HkAW2mH4QJ"]) + assert.deepEqual(lastBody.salty.pidx,1) + assert.deepEqual(lastBody.salty.kidx,0) + assert.deepEqual(lastBody.salty.stem,"signify:aid") + assert.deepEqual(lastBody.salty.tier,"low") + assert.deepEqual(lastBody.salty.icodes,["A","A","A"]) + assert.deepEqual(lastBody.salty.ncodes,["A","A","A"]) + assert.deepEqual(lastBody.salty.dcode,"E") + assert.deepEqual(lastBody.salty.transferable,true) + + await client.identifiers().rotate('aid1') + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + lastBody = JSON.parse(lastCall[1]!.body!.toString()) + assert.equal(lastCall[0]!,url+'/identifiers/aid1') + assert.equal(lastCall[1]!.method,'PUT') + assert.deepEqual(lastBody.rot,{"v":"KERI10JSON000160_","t":"rot","d":"EBQABdRgaxJONrSLcgrdtbASflkvLxJkiDO0H-XmuhGg","i":"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK","s":"1","p":"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK","kt":"1","k":["DHgomzINlGJHr-XP3sv2ZcR9QsIEYS3LJhs4KRaZYKly"],"nt":"1","n":["EJMovBlrBuD6BVeUsGSxLjczbLEbZU9YnTSud9K4nVzk"],"bt":"0","br":[],"ba":[],"a":[]}) + assert.deepEqual(lastBody.sigs,["AABWSckRpAWLpfFSrpnDR3SzQASrRSVKGh8AnHxauhN_43qKkqPb9l04utnTm2ixNpGGJ-UB8qdKMjfkEQ61AIQC"]) + assert.deepEqual(lastBody.salty.pidx,0) + assert.deepEqual(lastBody.salty.kidx,1) + assert.deepEqual(lastBody.salty.stem,"signify:aid") + assert.deepEqual(lastBody.salty.tier,"low") + assert.deepEqual(lastBody.salty.icodes,["A"]) + assert.deepEqual(lastBody.salty.ncodes,["A"]) + assert.deepEqual(lastBody.salty.dcode,"E") + assert.deepEqual(lastBody.salty.transferable,true) + + let data = [{i:"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK",s:0,d:"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK"}] + await client.identifiers().interact('aid1',data) + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + lastBody = JSON.parse(lastCall[1]!.body!.toString()) + assert.equal(lastCall[0]!,url+'/identifiers/aid1?type=ixn') + assert.equal(lastCall[1]!.method,'PUT') + assert.deepEqual(lastBody.ixn,{"v":"KERI10JSON000138_","t":"ixn","d":"EPtNJLDft3CB-oz3qIhe86fnTKs-GYWiWyx8fJv3VO5e","i":"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK","s":"1","p":"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK","a":[{"i":"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK","s":0,"d":"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK"}]}) + assert.deepEqual(lastBody.sigs,["AADEzKk-5LT6vH-PWFb_1i1A8FW-KGHORtTOCZrKF4gtWkCr9vN1z_mDSVKRc6MKktpdeB3Ub1fWCGpnS50hRgoJ"]) + + }) + + it('Randy identifiers', async () => { + await libsodium.ready; + const bran = "0123456789abcdefghijk" + + let client = new SignifyClient(url, bran, Tier.low, boot_url) + + await client.boot() + await client.connect() + + let identifiers = client.identifiers() + + await identifiers.list() + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + assert.equal(lastCall[0]!,url+'/identifiers') + assert.equal(lastCall[1]!.method,'GET') + + await client.identifiers().create('aid1', {bran: '0123456789abcdefghijk',algo: Algos.randy}) + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + let lastBody = JSON.parse(lastCall[1]!.body!.toString()) + assert.equal(lastCall[0]!,url+'/identifiers') + assert.equal(lastCall[1]!.method,'POST') + assert.equal(lastBody.name,'aid1') + assert.deepEqual(lastBody.icp.s,"0") + assert.deepEqual(lastBody.icp.kt,"1") + assert.deepEqual(lastBody.randy.transferable,true) + + }) + + it('Registries and schemas', async () => { + await libsodium.ready; + const bran = "0123456789abcdefghijk" + + let client = new SignifyClient(url, bran, Tier.low, boot_url) + + await client.boot() + await client.connect() + + let registries = client.registries() + + await registries.list("aid") + let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + assert.equal(lastCall[0]!,url+'/identifiers/aid/registries') + assert.equal(lastCall[1]!.method,'GET') + + await registries.create("aid", "reg1","ALGn4yvn-VoiEuKgSZcAyM-QyPHIZFHn9CKZz0DOI5ue") + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + let lastBody = JSON.parse(lastCall[1]!.body!.toString()) + assert.equal(lastCall[0]!,url+'/identifiers/aid/registries') + assert.equal(lastCall[1]!.method,'POST') + assert.equal(lastBody.name,"reg1") + assert.deepEqual(lastBody.vcp,{"v":"KERI10JSON000113_","t":"vcp","d":"EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p","i":"EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p","ii":"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK","s":"0","c":["NB"],"bt":"0","b":[],"n":"ALGn4yvn-VoiEuKgSZcAyM-QyPHIZFHn9CKZz0DOI5ue"}) + assert.deepEqual(lastBody.ixn,{"v":"KERI10JSON00013a_","t":"ixn","d":"EMMF0C7NyqdEUZwLhRqe6Ki4bEMwdmnDFKkejhQwQDUD","i":"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK","s":"1","p":"ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK","a":[{"i":"EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p","s":"0","d":"EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p"}]}) + assert.deepEqual(lastBody.sigs,["AABtw7U9CsMBd5Iq9j5SsQsHSK3-E85SjzWCqakyTVGbO_8UrSDXjg2a6O5xsDwu2rVjhs8HsHYjMu5mOoriWQgD"]) + + let schemas = client.schemas() + + await schemas.list() + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + assert.equal(lastCall[0]!,url+'/schema') + assert.equal(lastCall[1]!.method,'GET') + + const schemaSAID = "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao" + await schemas.get(schemaSAID) + lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length-1]! + assert.equal(lastCall[0]!,url+'/schema/'+schemaSAID) + assert.equal(lastCall[1]!.method,'GET') + + + }) + +}) \ No newline at end of file