From 5dda06153aaef9e452d948ec0b4ff81134b293f4 Mon Sep 17 00:00:00 2001 From: lenkan Date: Tue, 13 Feb 2024 21:42:29 +0100 Subject: [PATCH 01/18] fix sn to int parsing to use base 16 --- src/keri/app/aiding.ts | 4 ++-- src/keri/app/credentialing.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/keri/app/aiding.ts b/src/keri/app/aiding.ts index 9ce09417..a4e5269e 100644 --- a/src/keri/app/aiding.ts +++ b/src/keri/app/aiding.ts @@ -254,7 +254,7 @@ export class Identifier { const pre: string = hab.prefix; const state = hab.state; - const sn = Number(state.s); + const sn = parseInt(state.s, 16); const dig = state.d; data = Array.isArray(data) ? data : [data]; @@ -307,7 +307,7 @@ export class Identifier { const state = hab.state; const count = state.k.length; const dig = state.d; - const ridx = Number(state.s) + 1; + const ridx = parseInt(state.s, 16) + 1; const wits = state.b; let isith = state.nt; diff --git a/src/keri/app/credentialing.ts b/src/keri/app/credentialing.ts index 058ccdd4..ccdfe22b 100644 --- a/src/keri/app/credentialing.ts +++ b/src/keri/app/credentialing.ts @@ -224,7 +224,7 @@ export class Credentials { dt: dt, }); - const sn = Number(hab.state.s); + const sn = parseInt(hab.state.s, 16); const anc = interact({ pre: hab.prefix, sn: sn + 1, @@ -308,7 +308,7 @@ export class Credentials { var estOnly = false; } - const sn = Number(state.s); + const sn = parseInt(state.s, 16); const dig = state.d; const data: any = [ @@ -583,7 +583,7 @@ export class Registries { throw new Error('establishment only not implemented'); } else { const state = hab.state; - const sn = Number(state.s); + const sn = parseInt(state.s, 16); const dig = state.d; const data: any = [ From a646807f403ead57f471d0214690acc67d8449eb Mon Sep 17 00:00:00 2001 From: lenkan Date: Tue, 13 Feb 2024 21:58:38 +0100 Subject: [PATCH 02/18] add test for aid state s > 10 --- src/keri/app/aiding.ts | 2 -- test/app/aiding.test.ts | 65 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/keri/app/aiding.ts b/src/keri/app/aiding.ts index a4e5269e..c0bad4d1 100644 --- a/src/keri/app/aiding.ts +++ b/src/keri/app/aiding.ts @@ -259,8 +259,6 @@ export class Identifier { data = Array.isArray(data) ? data : [data]; - data = Array.isArray(data) ? data : [data]; - const serder = interact({ pre: pre, sn: sn + 1, diff --git a/test/app/aiding.test.ts b/test/app/aiding.test.ts index 094389d3..898ce65d 100644 --- a/test/app/aiding.test.ts +++ b/test/app/aiding.test.ts @@ -191,6 +191,30 @@ describe('Aiding', () => { assert.deepEqual(lastCall.body.salty.transferable, true); }); + it('Can rotate salty identifier with sn > 10', async () => { + const aid1 = await createMockIdentifierState('aid1', bran, {}); + client.fetch.mockResolvedValueOnce( + Response.json({ + ...aid1, + state: { + ...aid1.state, + s: 'a', + }, + }) + ); + client.fetch.mockResolvedValueOnce(Response.json({})); + + await client.identifiers().rotate('aid1'); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/identifiers/aid1'); + assert.equal(lastCall.method, 'PUT'); + expect(lastCall.body.rot).toMatchObject({ + v: 'KERI10JSON000160_', + t: 'rot', + s: 'b', + }); + }); + it('Can create interact event', async () => { const data = [ { @@ -217,13 +241,7 @@ describe('Aiding', () => { i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', s: '1', p: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', - a: [ - { - i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', - s: 0, - d: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', - }, - ], + a: data, }); assert.deepEqual(lastCall.body.sigs, [ @@ -231,6 +249,39 @@ describe('Aiding', () => { ]); }); + it('Can create interact event when sequence number > 10', async () => { + const data = [ + { + i: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + s: 0, + d: 'ELUvZ8aJEHAQE-0nsevyYTP98rBbGJUrTj5an-pCmwrK', + }, + ]; + + const aid1 = await createMockIdentifierState('aid1', bran); + client.fetch.mockResolvedValueOnce( + Response.json({ + ...aid1, + state: { + ...aid1.state, + s: 'a', + }, + }) + ); + client.fetch.mockResolvedValueOnce(Response.json({})); + + await client.identifiers().interact('aid1', data); + + const lastCall = client.getLastMockRequest(); + + expect(lastCall.path).toEqual('/identifiers/aid1?type=ixn'); + expect(lastCall.method).toEqual('PUT'); + expect(lastCall.body.ixn).toMatchObject({ + s: 'b', + a: data, + }); + }); + it('Can add end role', async () => { const aid1 = await createMockIdentifierState('aid1', bran, {}); client.fetch.mockResolvedValueOnce(Response.json(aid1)); From 9bae76deea0c8568b1d3e9379664ad6c03e1ce4d Mon Sep 17 00:00:00 2001 From: lenkan Date: Tue, 13 Feb 2024 22:01:16 +0100 Subject: [PATCH 03/18] fix string template --- src/keri/core/number.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/keri/core/number.ts b/src/keri/core/number.ts index 082a2e14..18a4d27d 100644 --- a/src/keri/core/number.ts +++ b/src/keri/core/number.ts @@ -40,7 +40,7 @@ export class CesrNumber extends Matter { // make huge version of code code = code = NumDex.Huge; } else { - throw new Error('Invalid num = {num}, too large to encode.'); + throw new Error(`Invalid num = ${num}, too large to encode.`); } raw = intToBytes(_num, Matter._rawSize(code)); From b9b1029bdd23b786cfde688ade302e5dd57fe5b8 Mon Sep 17 00:00:00 2001 From: lenkan Date: Wed, 14 Feb 2024 16:30:41 +0100 Subject: [PATCH 04/18] fix: remove window.Buffer assignment --- src/buffer-polyfill.ts | 5 ----- src/index.ts | 1 - src/keri/core/bexter.ts | 1 + src/keri/core/diger.ts | 2 +- src/keri/core/prefixer.ts | 2 +- src/keri/core/saider.ts | 2 +- 6 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 src/buffer-polyfill.ts diff --git a/src/buffer-polyfill.ts b/src/buffer-polyfill.ts deleted file mode 100644 index 7e3a6f00..00000000 --- a/src/buffer-polyfill.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Buffer } from 'buffer'; - -try { - window.Buffer = Buffer; -} catch (e) {} diff --git a/src/index.ts b/src/index.ts index 99e284bb..122d03b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -import './buffer-polyfill'; import * as exp from './exports'; export * from './exports'; export default exp; diff --git a/src/keri/core/bexter.ts b/src/keri/core/bexter.ts index bf1abfcb..9be0983f 100644 --- a/src/keri/core/bexter.ts +++ b/src/keri/core/bexter.ts @@ -1,6 +1,7 @@ import { BexDex, Matter, MatterArgs, MtrDex } from './matter'; import { EmptyMaterialError } from './kering'; import Base64 from 'urlsafe-base64'; +import { Buffer } from 'buffer'; const B64REX = '^[A-Za-z0-9\\-_]*$'; export const Reb64 = new RegExp(B64REX); diff --git a/src/keri/core/diger.ts b/src/keri/core/diger.ts index 8bcbfb22..2e5a32f7 100644 --- a/src/keri/core/diger.ts +++ b/src/keri/core/diger.ts @@ -1,5 +1,5 @@ import { blake3 } from '@noble/hashes/blake3'; - +import { Buffer } from 'buffer'; import { Matter, MatterArgs, MtrDex } from './matter'; /** diff --git a/src/keri/core/prefixer.ts b/src/keri/core/prefixer.ts index 09b99ccb..b2e9faff 100644 --- a/src/keri/core/prefixer.ts +++ b/src/keri/core/prefixer.ts @@ -3,7 +3,7 @@ import { EmptyMaterialError } from './kering'; import { Dict, Ilks } from './core'; import { sizeify } from './serder'; import { Verfer } from './verfer'; - +import { Buffer } from 'buffer'; import { blake3 } from '@noble/hashes/blake3'; const Dummy: string = '#'; diff --git a/src/keri/core/saider.ts b/src/keri/core/saider.ts index d2e55d90..306e228b 100644 --- a/src/keri/core/saider.ts +++ b/src/keri/core/saider.ts @@ -2,7 +2,7 @@ import { DigiDex, Matter, MatterArgs, MtrDex } from './matter'; import { deversify, Dict, Serials } from './core'; import { EmptyMaterialError } from './kering'; import { dumps, sizeify } from './serder'; - +import { Buffer } from 'buffer'; import { blake3 } from '@noble/hashes/blake3'; const Dummy = '#'; From a50faf50978ee537266f741f6f1050ef7bf4090c Mon Sep 17 00:00:00 2001 From: lenkan Date: Sat, 24 Feb 2024 11:51:04 +0100 Subject: [PATCH 05/18] chore: upgrade math.js --- package-lock.json | 30 +++++++++++++++--------------- package.json | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca324d4c..ca3d4cd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "signify-ts", - "version": "0.1.1", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "signify-ts", - "version": "0.1.1", + "version": "0.2.1", "license": "Apache-2.0", "workspaces": [ "examples/*" @@ -16,7 +16,7 @@ "buffer": "^6.0.3", "ecdsa-secp256r1": "^1.3.3", "libsodium-wrappers-sumo": "^0.7.9", - "mathjs": "^11.8.2", + "mathjs": "^12.4.0", "structured-headers": "^0.5.0", "urlsafe-base64": "^1.0.0" }, @@ -664,9 +664,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -6388,11 +6388,11 @@ } }, "node_modules/mathjs": { - "version": "11.11.0", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.11.0.tgz", - "integrity": "sha512-i1Ao/tv1mlNd09XlOMOUu3KMySX3S0jhHNfDPzh0sCnPf1i62x6RjxhLwZ9ytmVSs0OdhF3moI4O84VSEjmUFw==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.0.tgz", + "integrity": "sha512-4Moy0RNjwMSajEkGGxNUyMMC/CZAcl87WBopvNsJWB4E4EFebpTedr+0/rhqmnOSTH3Wu/3WfiWiw6mqiaHxVw==", "dependencies": { - "@babel/runtime": "^7.22.6", + "@babel/runtime": "^7.23.9", "complex.js": "^2.1.1", "decimal.js": "^10.4.3", "escape-latex": "^1.2.0", @@ -6400,13 +6400,13 @@ "javascript-natural-sort": "^0.7.1", "seedrandom": "^3.0.5", "tiny-emitter": "^2.1.0", - "typed-function": "^4.1.0" + "typed-function": "^4.1.1" }, "bin": { "mathjs": "bin/cli.js" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/md5.js": { @@ -7647,9 +7647,9 @@ } }, "node_modules/typed-function": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.0.tgz", - "integrity": "sha512-DGwUl6cioBW5gw2L+6SMupGwH/kZOqivy17E4nsh1JI9fKF87orMmlQx3KISQPmg3sfnOUGlwVkroosvgddrlg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.1.tgz", + "integrity": "sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==", "engines": { "node": ">= 14" } diff --git a/package.json b/package.json index b8e95a00..80a3e017 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "buffer": "^6.0.3", "ecdsa-secp256r1": "^1.3.3", "libsodium-wrappers-sumo": "^0.7.9", - "mathjs": "^11.8.2", + "mathjs": "^12.4.0", "structured-headers": "^0.5.0", "urlsafe-base64": "^1.0.0" }, From e2b2f6e3d9ecd4d1ae22e93ab82ded4ef366312b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lenksj=C3=B6?= <5889538+lenkan@users.noreply.github.com> Date: Fri, 1 Mar 2024 16:36:02 +0100 Subject: [PATCH 06/18] simplify prettier setup (#223) --- .jsdoc.json | 6 ++---- .prettierignore | 5 +++++ .prettierrc | 12 ++++++------ README.md | 13 +++++-------- codecov.yaml | 18 +++++++++--------- config/keria.json | 20 +++++++++----------- examples/integration-scripts/tsconfig.json | 12 ++++++------ package.json | 4 ++-- tsconfig.json | 12 ++++++------ tsconfig.node.json | 22 +++++++++++----------- types/index.d.ts | 2 +- 11 files changed, 62 insertions(+), 64 deletions(-) create mode 100644 .prettierignore diff --git a/.jsdoc.json b/.jsdoc.json index d0578e4e..894acd55 100644 --- a/.jsdoc.json +++ b/.jsdoc.json @@ -8,9 +8,7 @@ "includePattern": ".ts$", "excludePattern": "(node_modules/|docs|dist|examples|test)" }, - "plugins": [ - "plugins/markdown" - ], + "plugins": ["plugins/markdown"], "templates": { "cleverLinks": false, "monospaceLinks": true, @@ -24,4 +22,4 @@ "recurse": true, "template": "./node_modules/minami" } -} \ No newline at end of file +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..551153cb --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +diagrams/ +docs/ +package.json +package-lock.json +.github/ diff --git a/.prettierrc b/.prettierrc index 8875da91..ad9620ae 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { - "semi": true, - "trailingComma": "es5", - "singleQuote": true, - "printWidth": 80, - "tabWidth": 4 -} \ No newline at end of file + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 4 +} diff --git a/README.md b/README.md index fe4a70e6..22635f96 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Project Name: signify-ts ## Signify - KERI Signing at the Edge -Of the five functions in a KERI agent, +Of the five functions in a KERI agent, 1. Key generation 2. Encrypted key storage @@ -19,20 +19,18 @@ Of the five functions in a KERI agent, Signify-TS splits off two, key generation and event signing into a TypeScript library to provide "signing at the edge". It accomplishes this by using [libsodium](https://doc.libsodium.org/) to generate ed25519 key pairs for signing and x25519 key pairs for encrypting the -private keys, next public keys and salts used to generate the private keys. The encrypted private key and salts are then stored on a -remote cloud agent that never has access to the decryption keys. New key pair sets (current and next) will be generated +private keys, next public keys and salts used to generate the private keys. The encrypted private key and salts are then stored on a +remote cloud agent that never has access to the decryption keys. New key pair sets (current and next) will be generated for inception and rotation events with only the public keys and blake3 hash of the next keys made available to the agent. The communication protocol between a Signify client and [KERI](https://github.com/WebOfTrust/keri) agent will encode all cryptographic primitives as CESR base64 -encoded strings for the initial implementation. Support for binary CESR can be added in the future. - +encoded strings for the initial implementation. Support for binary CESR can be added in the future. ### Environment Setup The code is built using Typescript and running code locally requires a Mac or Linux OS. -- Install [Node.js](https://nodejs.org) - +- Install [Node.js](https://nodejs.org) - Install dependencies: ```bash @@ -117,5 +115,4 @@ Account Creation Workflow ![Account Creation](/diagrams/account-creation-workflow.png) - ![Account Creation Webpage](/diagrams/account-creation-webpage-workflow.png) diff --git a/codecov.yaml b/codecov.yaml index 7c2af1c8..45ec0ff1 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -1,10 +1,10 @@ coverage: - status: - project: - default: - target: 55 - paths: ["src"] - patch: - default: - target: 75 - paths: ["src"] \ No newline at end of file + status: + project: + default: + target: 55 + paths: ['src'] + patch: + default: + target: 75 + paths: ['src'] diff --git a/config/keria.json b/config/keria.json index 2fcc8194..3094cf42 100755 --- a/config/keria.json +++ b/config/keria.json @@ -1,17 +1,15 @@ { "dt": "2023-12-01T10:05:25.062609+00:00", "keria": { - "dt": "2023-12-01T10:05:25.062609+00:00", - "curls": [ - "http://keria:3902/" - ] + "dt": "2023-12-01T10:05:25.062609+00:00", + "curls": ["http://keria:3902/"] }, "iurls": [ - "http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller", - "http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller", - "http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller", - "http://witness-demo:5645/oobi/BM35JN8XeJSEfpxopjn5jr7tAHCE5749f0OobhMLCorE/controller", - "http://witness-demo:5646/oobi/BIj15u5V11bkbtAxMA7gcNJZcax-7TgaBMLsQnMHpYHP/controller", - "http://witness-demo:5647/oobi/BF2rZTW79z4IXocYRQnjjsOuvFUQv-ptCf8Yltd7PfsM/controller" + "http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller", + "http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller", + "http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller", + "http://witness-demo:5645/oobi/BM35JN8XeJSEfpxopjn5jr7tAHCE5749f0OobhMLCorE/controller", + "http://witness-demo:5646/oobi/BIj15u5V11bkbtAxMA7gcNJZcax-7TgaBMLsQnMHpYHP/controller", + "http://witness-demo:5647/oobi/BF2rZTW79z4IXocYRQnjjsOuvFUQv-ptCf8Yltd7PfsM/controller" ] - } \ No newline at end of file +} diff --git a/examples/integration-scripts/tsconfig.json b/examples/integration-scripts/tsconfig.json index 6c19b63b..e5423e88 100644 --- a/examples/integration-scripts/tsconfig.json +++ b/examples/integration-scripts/tsconfig.json @@ -1,9 +1,9 @@ { - "extends": "../../tsconfig.node.json", - "compilerOptions": { - "noEmit": true, - "paths": { - "signify-ts": ["../../src"] + "extends": "../../tsconfig.node.json", + "compilerOptions": { + "noEmit": true, + "paths": { + "signify-ts": ["../../src"] + } } - } } diff --git a/package.json b/package.json index 80a3e017..b1bddadf 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "lint": "npx eslint src test examples/integration-scripts", "prepare": "npm run build", "generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose", - "pretty": "prettier --config .prettierrc 'src/**/*.ts' 'test/**/*.ts' 'examples/**/*.ts' --write", - "pretty:check": "prettier --config .prettierrc 'src/**/*.ts' 'test/**/*.ts' 'examples/**/*.ts' --check" + "pretty": "prettier --write .", + "pretty:check": "prettier --check ." }, "name": "signify-ts", "author": "Phil Feairheller", diff --git a/tsconfig.json b/tsconfig.json index 75753aef..84df33bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { - "include": ["src", "types", "test", "examples/integration-scripts"], - "extends": "./tsconfig.node.json", - "compilerOptions": { - "noEmit": true, - "allowImportingTsExtensions": true - } + "include": ["src", "types", "test", "examples/integration-scripts"], + "extends": "./tsconfig.node.json", + "compilerOptions": { + "noEmit": true, + "allowImportingTsExtensions": true + } } diff --git a/tsconfig.node.json b/tsconfig.node.json index c305da5b..4f5481eb 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,13 +1,13 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "lib": ["dom", "esnext"], - "moduleResolution": "node", - "strict": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true - } + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["dom", "esnext"], + "moduleResolution": "node", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true + } } diff --git a/types/index.d.ts b/types/index.d.ts index 33e1af77..f9be4f0f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1 +1 @@ -declare module 'ecdsa-secp256r1'; \ No newline at end of file +declare module 'ecdsa-secp256r1'; From e8b66e9e01753549db3f4bae4bf31c18e38960dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lenksj=C3=B6?= <5889538+lenkan@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:19:53 +0100 Subject: [PATCH 07/18] fix: remove urlsafe-base64 dependency (#218) --- package-lock.json | 8 +------- package.json | 3 +-- src/keri/core/base64.ts | 28 ++++++++++++++++++++++++++++ src/keri/core/bexter.ts | 6 +++--- src/keri/core/httping.ts | 4 ++-- src/keri/core/indexer.ts | 8 ++++---- src/keri/core/matter.ts | 10 +++++----- test/core/base64.test.ts | 35 +++++++++++++++++++++++++++++++++++ test/core/indexer.test.ts | 8 ++++---- 9 files changed, 83 insertions(+), 27 deletions(-) create mode 100644 src/keri/core/base64.ts create mode 100644 test/core/base64.test.ts diff --git a/package-lock.json b/package-lock.json index ca3d4cd6..e4f9dcd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,7 @@ "ecdsa-secp256r1": "^1.3.3", "libsodium-wrappers-sumo": "^0.7.9", "mathjs": "^12.4.0", - "structured-headers": "^0.5.0", - "urlsafe-base64": "^1.0.0" + "structured-headers": "^0.5.0" }, "devDependencies": { "@mermaid-js/mermaid-cli": "^10.3.0", @@ -7797,11 +7796,6 @@ "punycode": "^2.1.0" } }, - "node_modules/urlsafe-base64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz", - "integrity": "sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA==" - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index b1bddadf..1c5e7644 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,7 @@ "ecdsa-secp256r1": "^1.3.3", "libsodium-wrappers-sumo": "^0.7.9", "mathjs": "^12.4.0", - "structured-headers": "^0.5.0", - "urlsafe-base64": "^1.0.0" + "structured-headers": "^0.5.0" }, "workspaces": [ "examples/*" diff --git a/src/keri/core/base64.ts b/src/keri/core/base64.ts new file mode 100644 index 00000000..ff81a475 --- /dev/null +++ b/src/keri/core/base64.ts @@ -0,0 +1,28 @@ +import { Buffer } from 'buffer'; +// base64url is supported by node Buffer, but not in buffer package for browser compatibility +// https://github.com/feross/buffer/issues/309 + +// Instead of using a node.js-only module and forcing us to polyfill the Buffer global, +// we insert code from https://gitlab.com/seangenabe/safe-base64 here + +export function encodeBase64Url(buffer: Buffer) { + if (!Buffer.isBuffer(buffer)) { + throw new TypeError('`buffer` must be a buffer.'); + } + return buffer + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+/, ''); +} + +export function decodeBase64Url(input: string) { + if (!(typeof input === 'string')) { + throw new TypeError('`input` must be a string.'); + } + + const n = input.length % 4; + const padded = input + '='.repeat(n > 0 ? 4 - n : n); + const base64String = padded.replace(/-/g, '+').replace(/_/g, '/'); + return Buffer.from(base64String, 'base64'); +} diff --git a/src/keri/core/bexter.ts b/src/keri/core/bexter.ts index 9be0983f..c484fbb5 100644 --- a/src/keri/core/bexter.ts +++ b/src/keri/core/bexter.ts @@ -1,7 +1,7 @@ import { BexDex, Matter, MatterArgs, MtrDex } from './matter'; import { EmptyMaterialError } from './kering'; -import Base64 from 'urlsafe-base64'; import { Buffer } from 'buffer'; +import { decodeBase64Url, encodeBase64Url } from './base64'; const B64REX = '^[A-Za-z0-9\\-_]*$'; export const Reb64 = new RegExp(B64REX); @@ -115,7 +115,7 @@ export class Bexter extends Matter { const wad = new Array(ws); wad.fill('A'); const base = wad.join('') + bext; // pre pad with wad of zeros in Base64 == 'A' - const raw = Base64.decode(base); // [ls:] // convert and remove leader + const raw = decodeBase64Url(base); // [ls:] // convert and remove leader return Uint8Array.from(raw).subarray(ls); // raw binary equivalent of text } @@ -123,7 +123,7 @@ export class Bexter extends Matter { get bext(): string { const sizage = Matter.Sizes.get(this.code); const wad = Uint8Array.from(new Array(sizage?.ls).fill(0)); - const bext = Base64.encode(Buffer.from([...wad, ...this.raw])); + const bext = encodeBase64Url(Buffer.from([...wad, ...this.raw])); let ws = 0; if (sizage?.ls === 0 && bext !== undefined) { diff --git a/src/keri/core/httping.ts b/src/keri/core/httping.ts index 70b59dd5..d83694ff 100644 --- a/src/keri/core/httping.ts +++ b/src/keri/core/httping.ts @@ -10,8 +10,8 @@ import { b } from './core'; import { Cigar } from './cigar'; import { nowUTC } from './utils'; import { Siger } from './siger'; -import Base64 from 'urlsafe-base64'; import { Buffer } from 'buffer'; +import { encodeBase64Url } from './base64'; export function normalize(header: string) { return header.trim(); @@ -121,7 +121,7 @@ export class Unqualified { } get qb64(): string { - return Base64.encode(Buffer.from(this._raw)); + return encodeBase64Url(Buffer.from(this._raw)); } get qb64b(): Uint8Array { diff --git a/src/keri/core/indexer.ts b/src/keri/core/indexer.ts index 8649ca55..09b7f0ae 100644 --- a/src/keri/core/indexer.ts +++ b/src/keri/core/indexer.ts @@ -1,7 +1,7 @@ import { EmptyMaterialError } from './kering'; import { b, b64ToInt, d, intToB64, readInt } from './core'; -import Base64 from 'urlsafe-base64'; import { Buffer } from 'buffer'; +import { decodeBase64Url, encodeBase64Url } from './base64'; export class IndexerCodex { Ed25519_Sig: string = 'A'; // Ed25519 sig appears same in both lists if any. @@ -399,7 +399,7 @@ export class Indexer { } const full = - both + Base64.encode(Buffer.from(bytes)).slice(ps - xizage.ls); + both + encodeBase64Url(Buffer.from(bytes)).slice(ps - xizage.ls); if (full.length != xizage.fs) { throw new Error(`Invalid code=${both} for raw size=${raw.length}.`); } @@ -474,7 +474,7 @@ export class Indexer { let raw; if (ps != 0) { const base = new Array(ps + 1).join('A') + qb64.slice(cs); - const paw = Base64.decode(base); // decode base to leave prepadded raw + const paw = decodeBase64Url(base); // decode base to leave prepadded raw const pi = readInt(paw.slice(0, ps)); // prepad as int if (pi & (2 ** pbs - 1)) { // masked pad bits non-zero @@ -485,7 +485,7 @@ export class Indexer { raw = paw.slice(ps); // strip off ps prepad paw bytes } else { const base = qb64.slice(cs); - const paw = Base64.decode(base); + const paw = decodeBase64Url(base); const li = readInt(paw.slice(0, xizage!.ls)); if (li != 0) { if (li == 1) { diff --git a/src/keri/core/matter.ts b/src/keri/core/matter.ts index 8abaedf7..cbbc0f85 100644 --- a/src/keri/core/matter.ts +++ b/src/keri/core/matter.ts @@ -1,9 +1,9 @@ import { EmptyMaterialError } from './kering'; import { intToB64, readInt } from './core'; -import Base64 from 'urlsafe-base64'; import { b, d } from './core'; import { Buffer } from 'buffer'; +import { decodeBase64Url, encodeBase64Url } from './base64'; export class Codex { has(prop: string): boolean { @@ -421,7 +421,7 @@ export class Matter { bytes[odx] = raw[i]; } - return both + Base64.encode(Buffer.from(bytes)); + return both + encodeBase64Url(Buffer.from(bytes)); } else { const both = code; const cs = both.length; @@ -443,7 +443,7 @@ export class Matter { bytes[odx] = raw[i]; } - return both + Base64.encode(Buffer.from(bytes)).slice(cs % 4); + return both + encodeBase64Url(Buffer.from(bytes)).slice(cs % 4); } } @@ -487,7 +487,7 @@ export class Matter { let raw; if (ps != 0) { const base = new Array(ps + 1).join('A') + qb64.slice(cs); - const paw = Base64.decode(base); // decode base to leave prepadded raw + const paw = decodeBase64Url(base); // decode base to leave prepadded raw const pi = readInt(paw.subarray(0, ps)); // prepad as int if (pi & (2 ** pbs - 1)) { // masked pad bits non-zero @@ -498,7 +498,7 @@ export class Matter { raw = paw.subarray(ps); // strip off ps prepad paw bytes } else { const base = qb64.slice(cs); - const paw = Base64.decode(base); + const paw = decodeBase64Url(base); const li = readInt(paw.subarray(0, sizage!.ls)); if (li != 0) { if (li == 1) { diff --git a/test/core/base64.test.ts b/test/core/base64.test.ts new file mode 100644 index 00000000..cc4f9488 --- /dev/null +++ b/test/core/base64.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert'; +import { decodeBase64Url, encodeBase64Url } from '../../src/keri/core/base64'; + +test('encode', () => { + assert.equal(encodeBase64Url(Buffer.from('f')), 'Zg'); + assert.equal(encodeBase64Url(Buffer.from('fi')), 'Zmk'); + assert.equal(encodeBase64Url(Buffer.from('fis')), 'Zmlz'); + assert.equal(encodeBase64Url(Buffer.from('fish')), 'ZmlzaA'); + assert.equal(encodeBase64Url(Buffer.from([248])), '-A'); + assert.equal(encodeBase64Url(Buffer.from([252])), '_A'); +}); + +test('decode', () => { + assert.equal(decodeBase64Url('Zg').toString(), 'f'); + assert.equal(decodeBase64Url('Zmk').toString(), 'fi'); + assert.equal(decodeBase64Url('Zmlz').toString(), 'fis'); + assert.equal(decodeBase64Url('ZmlzaA').toString(), 'fish'); + assert.equal(Buffer.from([248]).buffer, decodeBase64Url('-A').buffer); + assert.equal(Buffer.from([252]).buffer, decodeBase64Url('_A').buffer); +}); + +test('Test encode / decode compare with built in node Buffer', () => { + const text = '🏳️🏳️'; + const b64url = '8J-Ps--4j_Cfj7PvuI8'; + + assert.equal( + Buffer.from(text).toString('base64url'), + encodeBase64Url(Buffer.from(text)) + ); + + assert.equal( + Buffer.from(b64url, 'base64url').buffer, + decodeBase64Url(b64url).buffer + ); +}); diff --git a/test/core/indexer.test.ts b/test/core/indexer.test.ts index e0e53864..9186e82e 100644 --- a/test/core/indexer.test.ts +++ b/test/core/indexer.test.ts @@ -2,8 +2,8 @@ import libsodium from 'libsodium-wrappers-sumo'; import { strict as assert } from 'assert'; import { IdrDex, Indexer } from '../../src/keri/core/indexer'; import { b, intToB64 } from '../../src/keri/core/core'; -import Base64 from 'urlsafe-base64'; import { Buffer } from 'buffer'; +import { decodeBase64Url, encodeBase64Url } from '../../src/keri/core/base64'; describe('Indexer', () => { it('should encode and decode dual indexed signatures', async () => { @@ -68,7 +68,7 @@ describe('Indexer', () => { const odx = i + ps; bytes[odx] = sig[i]; } - const sig64 = Base64.encode(Buffer.from(bytes)); + const sig64 = encodeBase64Url(Buffer.from(bytes)); assert.equal(sig64.length, 88); assert.equal( sig64, @@ -85,7 +85,7 @@ describe('Indexer', () => { assert.equal(qsig64.length, 88); let qsig64b = b(qsig64); - let qsig2b = Base64.decode(qsig64); + let qsig2b = decodeBase64Url(qsig64); assert.equal(qsig2b.length, 66); // assert qsig2b == (b"\x00\x00\x99\xd2<9$$0\x9fk\xfb\x18\xa0\x8c@r\x122.k\xb2\xc7\x1fp\x0e'm" // b'\x8f@\xaa\xa5\x8c\xc8n\x85\xc8!\xf6q\x91p\xa9\xec\xcf\x92\xaf)' @@ -166,7 +166,7 @@ describe('Indexer', () => { qsig64 = 'AFCZ0jw5JCQwn2v7GKCMQHISMi5rsscfcA4nbY9AqqWMyG6FyCH2cZFwqezPkq8p3sr8f37Xb3wXgh3UPG8igSYJ'; qsig64b = b(qsig64); - qsig2b = Base64.decode(qsig64); + qsig2b = decodeBase64Url(qsig64); assert.equal(qsig2b.length, 66); indexer = new Indexer({ raw: sig, code: IdrDex.Ed25519_Sig, index: 5 }); From 4af063e0f869bd50f7a75a9693f78349f8d7aeb9 Mon Sep 17 00:00:00 2001 From: Lance <2byrds@gmail.com> Date: Thu, 28 Mar 2024 10:12:45 -0400 Subject: [PATCH 08/18] update to newer role name (#238) Signed-off-by: 2byrds <2byrds@gmail.com> --- examples/integration-scripts/singlesig-vlei-issuance.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/integration-scripts/singlesig-vlei-issuance.test.ts b/examples/integration-scripts/singlesig-vlei-issuance.test.ts index 7eee0d1b..b077d2bc 100644 --- a/examples/integration-scripts/singlesig-vlei-issuance.test.ts +++ b/examples/integration-scripts/singlesig-vlei-issuance.test.ts @@ -44,7 +44,7 @@ const leData = { const ecrData = { LEI: leData.LEI, personLegalName: 'John Doe', - engagementContextRole: 'EBA Submitter', + engagementContextRole: 'EBA Data Submitter', }; const ecrAuthData = { From a49db7f3c39e687d3a7d6a3751b47358cfd66653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lenksj=C3=B6?= <5889538+lenkan@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:15:40 +0100 Subject: [PATCH 09/18] fix: cannot set salts for credential creation (suggestion) (#222) * fix: cannot set different salts for credential * fix types * add comments * fix dt for issuance event --- .../integration-scripts/credentials.test.ts | 66 ++++++++------- .../multisig-holder.test.ts | 32 ++++---- examples/integration-scripts/multisig.test.ts | 36 +++----- .../singlesig-vlei-issuance.test.ts | 24 +++--- src/keri/app/credentialing.ts | 82 +++++++++---------- test/app/credentialing.test.ts | 13 +-- 6 files changed, 115 insertions(+), 138 deletions(-) diff --git a/examples/integration-scripts/credentials.test.ts b/examples/integration-scripts/credentials.test.ts index 1babbc26..e1e871d7 100644 --- a/examples/integration-scripts/credentials.test.ts +++ b/examples/integration-scripts/credentials.test.ts @@ -143,13 +143,16 @@ test('single signature credentials', async () => { LEI: '5493001KJTIIGC8Y1R17', }; - const issResult = await issuerClient.credentials().issue({ - issuerName: issuerAid.name, - registryId: registry.regk, - schemaId: QVI_SCHEMA_SAID, - recipient: holderAid.prefix, - data: vcdata, - }); + const issResult = await issuerClient + .credentials() + .issue(issuerAid.name, { + ri: registry.regk, + s: QVI_SCHEMA_SAID, + a: { + i: holderAid.prefix, + ...vcdata, + }, + }); await waitOperation(issuerClient, issResult.op); return issResult.acdc.ked.d as string; @@ -374,31 +377,32 @@ test('single signature credentials', async () => { .credentials() .get(qviCredentialId); - const result = await holderClient.credentials().issue({ - issuerName: holderAid.name, - recipient: legalEntityAid.prefix, - registryId: holderRegistry.regk, - schemaId: LE_SCHEMA_SAID, - data: { - LEI: '5493001KJTIIGC8Y1R17', - }, - rules: Saider.saidify({ - d: '', - usageDisclaimer: { - l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.', - }, - issuanceDisclaimer: { - l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.', - }, - })[1], - source: Saider.saidify({ - d: '', - qvi: { - n: qviCredential.sad.d, - s: qviCredential.sad.s, + const result = await holderClient + .credentials() + .issue(holderAid.name, { + a: { + i: legalEntityAid.prefix, + LEI: '5493001KJTIIGC8Y1R17', }, - })[1], - }); + ri: holderRegistry.regk, + s: LE_SCHEMA_SAID, + r: Saider.saidify({ + d: '', + usageDisclaimer: { + l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.', + }, + issuanceDisclaimer: { + l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.', + }, + })[1], + e: Saider.saidify({ + d: '', + qvi: { + n: qviCredential.sad.d, + s: qviCredential.sad.s, + }, + })[1], + }); await waitOperation(holderClient, result.op); return result.acdc.ked.d; diff --git a/examples/integration-scripts/multisig-holder.test.ts b/examples/integration-scripts/multisig-holder.test.ts index 1af254f9..b5e26879 100644 --- a/examples/integration-scripts/multisig-holder.test.ts +++ b/examples/integration-scripts/multisig-holder.test.ts @@ -1,9 +1,5 @@ import { strict as assert } from 'assert'; -import signify, { - SignifyClient, - IssueCredentialArgs, - Operation, -} from 'signify-ts'; +import signify, { SignifyClient, Operation, CredentialData } from 'signify-ts'; import { resolveEnvironment } from './utils/resolve-env'; import { assertOperations, @@ -354,12 +350,11 @@ test('multisig', async function run() { console.log(`Issuer starting credential issuance to holder...`); const registires = await client3.registries().list('issuer'); - await issueCredential(client3, { - issuerName: 'issuer', - registryId: registires[0].regk, - schemaId: SCHEMA_SAID, - recipient: holderAid['prefix'], - data: { + await issueCredential(client3, 'issuer', { + ri: registires[0].regk, + s: SCHEMA_SAID, + a: { + i: holderAid['prefix'], LEI: '5493001KJTIIGC8Y1R17', }, }); @@ -476,23 +471,24 @@ async function createRegistry( async function issueCredential( client: SignifyClient, - args: IssueCredentialArgs + name: string, + data: CredentialData ) { - const result = await client.credentials().issue(args); + const result = await client.credentials().issue(name, data); await waitOperation(client, result.op); const creds = await client.credentials().list(); assert.equal(creds.length, 1); - assert.equal(creds[0].sad.s, args.schemaId); + assert.equal(creds[0].sad.s, data.s); assert.equal(creds[0].status.s, '0'); const dt = createTimestamp(); - if (args.recipient) { + if (data.a.i) { const [grant, gsigs, end] = await client.ipex().grant({ - senderName: args.issuerName, - recipient: args.recipient, + senderName: name, + recipient: data.a.i, datetime: dt, acdc: result.acdc, anc: result.anc, @@ -501,7 +497,7 @@ async function issueCredential( let op = await client .ipex() - .submitGrant(args.issuerName, grant, gsigs, end, [args.recipient]); + .submitGrant(name, grant, gsigs, end, [data.a.i]); op = await waitOperation(client, op); } diff --git a/examples/integration-scripts/multisig.test.ts b/examples/integration-scripts/multisig.test.ts index ae03baaf..79f9d789 100644 --- a/examples/integration-scripts/multisig.test.ts +++ b/examples/integration-scripts/multisig.test.ts @@ -797,7 +797,6 @@ test('multisig', async function run() { }); op2 = await vcpRes2.op(); serder = vcpRes2.regser; - const regk2 = serder.pre; anc = vcpRes2.serder; sigs = vcpRes2.sigs; @@ -840,7 +839,6 @@ test('multisig', async function run() { }); op3 = await vcpRes3.op(); serder = vcpRes3.regser; - const regk3 = serder.pre; anc = vcpRes3.serder; sigs = vcpRes3.sigs; @@ -881,13 +879,14 @@ test('multisig', async function run() { const holder = aid4.prefix; const TIME = new Date().toISOString().replace('Z', '000+00:00'); - const credRes = await client1.credentials().issue({ - issuerName: 'multisig', - registryId: regk, - schemaId: SCHEMA_SAID, - data: vcdata, - recipient: holder, - datetime: TIME, + const credRes = await client1.credentials().issue('multisig', { + ri: regk, + s: SCHEMA_SAID, + a: { + i: holder, + dt: TIME, + ...vcdata, + }, }); op1 = credRes.op; await multisigIssue(client1, 'member1', 'multisig', credRes); @@ -905,15 +904,7 @@ test('multisig', async function run() { exn = res[0].exn; const credentialSaid = exn.e.acdc.d; - - const credRes2 = await client2.credentials().issue({ - issuerName: 'multisig', - registryId: regk2, - schemaId: SCHEMA_SAID, - data: vcdata, - datetime: exn.e.acdc.a.dt, - recipient: holder, - }); + const credRes2 = await client2.credentials().issue('multisig', exn.e.acdc); op2 = credRes2.op; await multisigIssue(client2, 'member2', 'multisig', credRes2); @@ -927,14 +918,7 @@ test('multisig', async function run() { res = await client3.groups().getRequest(msgSaid); exn = res[0].exn; - const credRes3 = await client3.credentials().issue({ - issuerName: 'multisig', - registryId: regk3, - schemaId: SCHEMA_SAID, - recipient: holder, - data: vcdata, - datetime: exn.e.acdc.a.dt, - }); + const credRes3 = await client3.credentials().issue('multisig', exn.e.acdc); op3 = credRes3.op; await multisigIssue(client3, 'member3', 'multisig', credRes3); diff --git a/examples/integration-scripts/singlesig-vlei-issuance.test.ts b/examples/integration-scripts/singlesig-vlei-issuance.test.ts index b077d2bc..62d7200b 100644 --- a/examples/integration-scripts/singlesig-vlei-issuance.test.ts +++ b/examples/integration-scripts/singlesig-vlei-issuance.test.ts @@ -1,5 +1,5 @@ import { strict as assert } from 'assert'; -import { Saider, Serder, SignifyClient } from 'signify-ts'; +import { Saider, Salter, Serder, SignifyClient } from 'signify-ts'; import { resolveEnvironment } from './utils/resolve-env'; import { assertOperations, @@ -491,7 +491,7 @@ async function getOrIssueCredential( schema: string, rules?: any, source?: any, - privacy: boolean = false + privacy = false ): Promise { const credentialList = await issuerClient.credentials().list(); @@ -507,15 +507,17 @@ async function getOrIssueCredential( if (credential) return credential; } - const issResult = await issuerClient.credentials().issue({ - issuerName: issuerAid.name, - registryId: issuerRegistry.regk, - schemaId: schema, - recipient: recipientAid.prefix, - data: credData, - rules: rules, - source: source, - privacy: privacy, + const issResult = await issuerClient.credentials().issue(issuerAid.name, { + ri: issuerRegistry.regk, + s: schema, + u: privacy ? new Salter({}).qb64 : undefined, + a: { + i: recipientAid.prefix, + u: privacy ? new Salter({}).qb64 : undefined, + ...credData, + }, + r: rules, + e: source, }); await waitOperation(issuerClient, issResult.op); diff --git a/src/keri/app/credentialing.ts b/src/keri/app/credentialing.ts index 28b11ab2..4d231087 100644 --- a/src/keri/app/credentialing.ts +++ b/src/keri/app/credentialing.ts @@ -36,51 +36,53 @@ export interface CredentialFilter { limit?: number; } -export interface IssueCredentialArgs { +export interface CredentialSubject { /** - * Name of the issuer identifier + * Issuee, or holder of the credential. */ - issuerName: string; - + i?: string; /** - * QB64 AID of credential registry + * Timestamp of issuance. */ - registryId: string; - + dt?: string; /** - * SAID Of the schema + * Privacy salt */ - schemaId: string; + u?: string; + [key: string]: unknown; +} +export interface CredentialData { + v?: string; + d?: string; /** - * Prefix of recipient identifier + * Privacy salt */ - recipient?: string; - + u?: string; /** - * Credential data + * Issuer of the credential. */ - data?: Record; - + i?: string; /** - * Credential rules + * Registry id. */ - rules?: string | Record; - + ri?: string; /** - * Credential sources + * Schema id */ - source?: Record; - + s?: string; /** - * Datetime to set for the credential + * Credential subject data */ - datetime?: string; - + a: CredentialSubject; /** - * Flag to issue a credential with privacy preserving features + * Credential source section */ - privacy?: boolean; + e?: { [key: string]: unknown }; + /** + * Credential rules section + */ + r?: { [key: string]: unknown }; } export interface IssueCredentialResult { @@ -184,8 +186,11 @@ export class Credentials { /** * Issue a credential */ - async issue(args: IssueCredentialArgs): Promise { - const hab = await this.client.identifiers().get(args.issuerName); + async issue( + name: string, + args: CredentialData + ): Promise { + const hab = await this.client.identifiers().get(name); const estOnly = hab.state.c !== undefined && hab.state.c.includes('EO'); if (estOnly) { // TODO implement rotation event @@ -197,27 +202,18 @@ export class Credentials { const keeper = this.client.manager.get(hab); - const dt = - args.datetime ?? new Date().toISOString().replace('Z', '000+00:00'); - const [, subject] = Saider.saidify({ d: '', - u: args.privacy ? new Salter({}).qb64 : undefined, - i: args.recipient, - dt: dt, - ...args.data, + ...args.a, + dt: args.a.dt ?? new Date().toISOString().replace('Z', '000+00:00'), }); const [, acdc] = Saider.saidify({ v: versify(Ident.ACDC, undefined, Serials.JSON, 0), d: '', - u: args.privacy ? new Salter({}).qb64 : undefined, - i: hab.prefix, - ri: args.registryId, - s: args.schemaId, + i: args.i ?? hab.prefix, + ...args, a: subject, - e: args.source, - r: args.rules, }); const [, iss] = Saider.saidify({ @@ -226,8 +222,8 @@ export class Credentials { d: '', i: acdc.d, s: '0', - ri: args.registryId, - dt: dt, + ri: args.ri, + dt: subject.dt, }); const sn = parseInt(hab.state.s, 16); diff --git a/test/app/credentialing.test.ts b/test/app/credentialing.test.ts index e426a041..3e822bc0 100644 --- a/test/app/credentialing.test.ts +++ b/test/app/credentialing.test.ts @@ -209,15 +209,10 @@ describe('Credentialing', () => { const registry = 'EP10ooRj0DJF0HWZePEYMLPl-arMV-MAoTKK-o3DXbgX'; const schema = 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'; const isuee = 'EG2XjQN-3jPN5rcR4spLjaJyM4zA6Lgg-Hd5vSMymu5p'; - await credentials.issue({ - issuerName: 'aid1', - registryId: registry, - schemaId: schema, - recipient: isuee, - data: { LEI: '1234' }, - source: {}, - rules: {}, - privacy: false, + await credentials.issue('aid1', { + ri: registry, + s: schema, + a: { i: isuee, LEI: '1234' }, }); lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; lastBody = JSON.parse(lastCall[1]!.body!.toString()); From ac6dac44cec401f6d95df19f3e2b475d9367995a Mon Sep 17 00:00:00 2001 From: Rodolfo Date: Wed, 3 Apr 2024 10:59:05 -0300 Subject: [PATCH 10/18] URL enconding of AID alias name in rest calls (#233) * URL enconding of name in api call * prettier * content-lenght header not needed * add test * remove debris * fix test * pretty * Update test/app/aiding.test.ts --- src/keri/app/aiding.ts | 2 +- src/keri/app/clienting.ts | 4 +--- test/app/aiding.test.ts | 9 +++++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/keri/app/aiding.ts b/src/keri/app/aiding.ts index c0bad4d1..f508f182 100644 --- a/src/keri/app/aiding.ts +++ b/src/keri/app/aiding.ts @@ -112,7 +112,7 @@ export class Identifier { * @returns {Promise} A promise to the identifier information */ async get(name: string): Promise { - const path = `/identifiers/${name}`; + const path = `/identifiers/${encodeURIComponent(name)}`; const data = null; const method = 'GET'; const res = await this.client.fetch(path, method, data); diff --git a/src/keri/app/clienting.ts b/src/keri/app/clienting.ts index 20b8e8d2..de2e5986 100644 --- a/src/keri/app/clienting.ts +++ b/src/keri/app/clienting.ts @@ -182,9 +182,7 @@ export class SignifyClient { headers.set('Content-Type', 'application/json'); const _body = method == 'GET' ? null : JSON.stringify(data); - if (_body !== null) { - headers.set('Content-Length', String(_body.length)); - } + if (this.authn) { signed_headers = this.authn.sign( headers, diff --git a/test/app/aiding.test.ts b/test/app/aiding.test.ts index 898ce65d..b2e333f9 100644 --- a/test/app/aiding.test.ts +++ b/test/app/aiding.test.ts @@ -99,6 +99,15 @@ describe('Aiding', () => { assert.deepEqual(lastCall.body.salty.transferable, true); }); + it('Can get identifiers with special characters in the name', async () => { + client.fetch.mockResolvedValue(Response.json({})); + await client.identifiers().get('a name with ñ!'); + + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.method, 'GET'); + assert.equal(lastCall.path, '/identifiers/a%20name%20with%20%C3%B1!'); + }); + it('Can create salty AID with multiple signatures', async () => { client.fetch.mockResolvedValue(Response.json({})); From 65d5b8398a25034234b6ed50965229ee5d446146 Mon Sep 17 00:00:00 2001 From: Nuttawut Kongsuwan Date: Fri, 5 Apr 2024 19:57:33 +0700 Subject: [PATCH 11/18] add integration test for multisig vlei (#207) Signed-off-by: Nuttawut Kongsuwan --- .../multisig-vlei-issuance.test.ts | 1675 +++++++++++++++++ .../singlesig-vlei-issuance.test.ts | 2 - 2 files changed, 1675 insertions(+), 2 deletions(-) create mode 100644 examples/integration-scripts/multisig-vlei-issuance.test.ts diff --git a/examples/integration-scripts/multisig-vlei-issuance.test.ts b/examples/integration-scripts/multisig-vlei-issuance.test.ts new file mode 100644 index 00000000..f6a7b06a --- /dev/null +++ b/examples/integration-scripts/multisig-vlei-issuance.test.ts @@ -0,0 +1,1675 @@ +import { strict as assert } from 'assert'; +import signify, { + SignifyClient, + Saider, + Serder, + CredentialSubject, + CredentialData, + CreateIdentiferArgs, + EventResult, + randomNonce, + Salter, +} from 'signify-ts'; +import { resolveEnvironment } from './utils/resolve-env'; +import { + resolveOobi, + waitOperation, + waitForNotifications, +} from './utils/test-util'; +import { getOrCreateClients, getOrCreateContact } from './utils/test-setup'; + +const { vleiServerUrl, witnessIds } = resolveEnvironment(); + +const QVI_SCHEMA_SAID = 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao'; +const LE_SCHEMA_SAID = 'ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY'; +const ECR_SCHEMA_SAID = 'EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw'; + +const vLEIServerHostUrl = `${vleiServerUrl}/oobi`; +const QVI_SCHEMA_URL = `${vLEIServerHostUrl}/${QVI_SCHEMA_SAID}`; +const LE_SCHEMA_URL = `${vLEIServerHostUrl}/${LE_SCHEMA_SAID}`; +const ECR_SCHEMA_URL = `${vLEIServerHostUrl}/${ECR_SCHEMA_SAID}`; + +const qviData = { + LEI: '254900OPPU84GM83MG36', +}; + +const leData = { + LEI: '875500ELOZEL05BVXV37', +}; + +const ecrData = { + LEI: leData.LEI, + personLegalName: 'John Doe', + engagementContextRole: 'EBA Submitter', +}; + +const LE_RULES = Saider.saidify({ + d: '', + usageDisclaimer: { + l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.', + }, + issuanceDisclaimer: { + l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.', + }, +})[1]; + +const ECR_RULES = Saider.saidify({ + d: '', + usageDisclaimer: { + l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.', + }, + issuanceDisclaimer: { + l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.', + }, + privacyDisclaimer: { + l: 'It is the sole responsibility of Holders as Issuees of an ECR vLEI Credential to present that Credential in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification.', + }, +})[1]; + +interface Aid { + name: string; + prefix: string; + state: any; +} + +test('multisig-vlei-issuance', async function run() { + /** + * The abbreviations used in this script follows GLEIF vLEI + * ecosystem governance framework (EGF). + * GEDA: GLEIF External Delegated AID + * QVI: Qualified vLEI Issuer + * LE: Legal Entity + * GAR: GLEIF Authorized Representative + * QAR: Qualified vLEI Issuer Authorized Representative + * LAR: Legal Entity Authorized Representative + * ECR: Engagement Context Role Person + */ + + const [ + clientGAR1, + clientGAR2, + clientQAR1, + clientQAR2, + clientQAR3, + clientLAR1, + clientLAR2, + clientLAR3, + clientECR, + ] = await getOrCreateClients(9); + + const kargsAID = { + toad: witnessIds.length, + wits: witnessIds, + }; + const [ + aidGAR1, + aidGAR2, + aidQAR1, + aidQAR2, + aidQAR3, + aidLAR1, + aidLAR2, + aidLAR3, + aidECR, + ] = await Promise.all([ + getOrCreateAID(clientGAR1, 'GAR1', kargsAID), + getOrCreateAID(clientGAR2, 'GAR2', kargsAID), + getOrCreateAID(clientQAR1, 'QAR1', kargsAID), + getOrCreateAID(clientQAR2, 'QAR2', kargsAID), + getOrCreateAID(clientQAR3, 'QAR3', kargsAID), + getOrCreateAID(clientLAR1, 'LAR1', kargsAID), + getOrCreateAID(clientLAR2, 'LAR2', kargsAID), + getOrCreateAID(clientLAR3, 'LAR3', kargsAID), + getOrCreateAID(clientECR, 'ECR', kargsAID), + ]); + + const [ + oobiGAR1, + oobiGAR2, + oobiQAR1, + oobiQAR2, + oobiQAR3, + oobiLAR1, + oobiLAR2, + oobiLAR3, + oobiECR, + ] = await Promise.all([ + clientGAR1.oobis().get('GAR1', 'agent'), + clientGAR2.oobis().get('GAR2', 'agent'), + clientQAR1.oobis().get('QAR1', 'agent'), + clientQAR2.oobis().get('QAR2', 'agent'), + clientQAR3.oobis().get('QAR3', 'agent'), + clientLAR1.oobis().get('LAR1', 'agent'), + clientLAR2.oobis().get('LAR2', 'agent'), + clientLAR3.oobis().get('LAR3', 'agent'), + clientECR.oobis().get('ECR', 'agent'), + ]); + + await Promise.all([ + getOrCreateContact(clientGAR1, 'GAR2', oobiGAR2.oobis[0]), + getOrCreateContact(clientGAR2, 'GAR1', oobiGAR1.oobis[0]), + getOrCreateContact(clientQAR1, 'QAR2', oobiQAR2.oobis[0]), + getOrCreateContact(clientQAR1, 'QAR3', oobiQAR3.oobis[0]), + getOrCreateContact(clientQAR2, 'QAR1', oobiQAR1.oobis[0]), + getOrCreateContact(clientQAR2, 'QAR3', oobiQAR3.oobis[0]), + getOrCreateContact(clientQAR3, 'QAR1', oobiQAR1.oobis[0]), + getOrCreateContact(clientQAR3, 'QAR2', oobiQAR2.oobis[0]), + getOrCreateContact(clientLAR1, 'LAR2', oobiLAR2.oobis[0]), + getOrCreateContact(clientLAR1, 'LAR3', oobiLAR3.oobis[0]), + getOrCreateContact(clientLAR2, 'LAR1', oobiLAR1.oobis[0]), + getOrCreateContact(clientLAR2, 'LAR3', oobiLAR3.oobis[0]), + getOrCreateContact(clientLAR3, 'LAR1', oobiLAR1.oobis[0]), + getOrCreateContact(clientLAR3, 'LAR2', oobiLAR2.oobis[0]), + getOrCreateContact(clientLAR1, 'ECR', oobiECR.oobis[0]), + getOrCreateContact(clientLAR2, 'ECR', oobiECR.oobis[0]), + getOrCreateContact(clientLAR3, 'ECR', oobiECR.oobis[0]), + ]); + + await Promise.all([ + resolveOobi(clientGAR1, QVI_SCHEMA_URL), + resolveOobi(clientGAR2, QVI_SCHEMA_URL), + resolveOobi(clientQAR1, QVI_SCHEMA_URL), + resolveOobi(clientQAR1, LE_SCHEMA_URL), + resolveOobi(clientQAR2, QVI_SCHEMA_URL), + resolveOobi(clientQAR2, LE_SCHEMA_URL), + resolveOobi(clientQAR3, QVI_SCHEMA_URL), + resolveOobi(clientQAR3, LE_SCHEMA_URL), + resolveOobi(clientLAR1, QVI_SCHEMA_URL), + resolveOobi(clientLAR1, LE_SCHEMA_URL), + resolveOobi(clientLAR1, ECR_SCHEMA_URL), + resolveOobi(clientLAR2, QVI_SCHEMA_URL), + resolveOobi(clientLAR2, LE_SCHEMA_URL), + resolveOobi(clientLAR2, ECR_SCHEMA_URL), + resolveOobi(clientLAR3, QVI_SCHEMA_URL), + resolveOobi(clientLAR3, LE_SCHEMA_URL), + resolveOobi(clientLAR3, ECR_SCHEMA_URL), + resolveOobi(clientECR, QVI_SCHEMA_URL), + resolveOobi(clientECR, LE_SCHEMA_URL), + resolveOobi(clientECR, ECR_SCHEMA_URL), + ]); + + // Create a multisig AID for the GEDA. + // Skip if a GEDA AID has already been incepted. + let aidGEDAbyGAR1, aidGEDAbyGAR2: Aid; + try { + aidGEDAbyGAR1 = await clientGAR1.identifiers().get('GEDA'); + aidGEDAbyGAR2 = await clientGAR2.identifiers().get('GEDA'); + } catch { + const rstates = [aidGAR1.state, aidGAR2.state]; + const states = rstates; + + const kargsMultisigAID: CreateIdentiferArgs = { + algo: signify.Algos.group, + isith: ['1/2', '1/2'], + nsith: ['1/2', '1/2'], + toad: kargsAID.toad, + wits: kargsAID.wits, + states: states, + rstates: rstates, + }; + + kargsMultisigAID.mhab = aidGAR1; + const multisigAIDOp1 = await createAIDMultisig( + clientGAR1, + aidGAR1, + [aidGAR2], + 'GEDA', + kargsMultisigAID, + true + ); + kargsMultisigAID.mhab = aidGAR2; + const multisigAIDOp2 = await createAIDMultisig( + clientGAR2, + aidGAR2, + [aidGAR1], + 'GEDA', + kargsMultisigAID + ); + + await Promise.all([ + waitOperation(clientGAR1, multisigAIDOp1), + waitOperation(clientGAR2, multisigAIDOp2), + ]); + + await waitAndMarkNotification(clientGAR1, '/multisig/icp'); + + aidGEDAbyGAR1 = await clientGAR1.identifiers().get('GEDA'); + aidGEDAbyGAR2 = await clientGAR2.identifiers().get('GEDA'); + } + assert.equal(aidGEDAbyGAR1.prefix, aidGEDAbyGAR2.prefix); + assert.equal(aidGEDAbyGAR1.name, aidGEDAbyGAR2.name); + const aidGEDA = aidGEDAbyGAR1; + + // Add endpoint role authorization for all GARs' agents. + // Skip if they have already been authorized. + let [oobiGEDAbyGAR1, oobiGEDAbyGAR2] = await Promise.all([ + clientGAR1.oobis().get(aidGEDA.name, 'agent'), + clientGAR2.oobis().get(aidGEDA.name, 'agent'), + ]); + if (oobiGEDAbyGAR1.oobis.length == 0 || oobiGEDAbyGAR2.oobis.length == 0) { + const timestamp = createTimestamp(); + const opList1 = await addEndRoleMultisig( + clientGAR1, + aidGAR1, + [aidGAR2], + aidGEDA, + timestamp, + true + ); + const opList2 = await addEndRoleMultisig( + clientGAR2, + aidGAR2, + [aidGAR1], + aidGEDA, + timestamp + ); + + await Promise.all(opList1.map((op) => waitOperation(clientGAR1, op))); + await Promise.all(opList2.map((op) => waitOperation(clientGAR2, op))); + + await waitAndMarkNotification(clientGAR1, '/multisig/rpy'); + + [oobiGEDAbyGAR1, oobiGEDAbyGAR2] = await Promise.all([ + clientGAR1.oobis().get(aidGEDA.name, 'agent'), + clientGAR2.oobis().get(aidGEDA.name, 'agent'), + ]); + } + assert.equal(oobiGEDAbyGAR1.role, oobiGEDAbyGAR2.role); + assert.equal(oobiGEDAbyGAR1.oobis[0], oobiGEDAbyGAR2.oobis[0]); + + // QARs, LARs, ECR resolve GEDA's OOBI + const oobiGEDA = oobiGEDAbyGAR1.oobis[0].split('/agent/')[0]; + await Promise.all([ + getOrCreateContact(clientQAR1, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientQAR2, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientQAR3, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientLAR1, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientLAR2, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientLAR3, aidGEDA.name, oobiGEDA), + getOrCreateContact(clientECR, aidGEDA.name, oobiGEDA), + ]); + + // Create a multisig AID for the QVI. + // Skip if a QVI AID has already been incepted. + let aidQVIbyQAR1, aidQVIbyQAR2, aidQVIbyQAR3: Aid; + try { + aidQVIbyQAR1 = await clientQAR1.identifiers().get('QVI'); + aidQVIbyQAR2 = await clientQAR2.identifiers().get('QVI'); + aidQVIbyQAR3 = await clientQAR3.identifiers().get('QVI'); + } catch { + const rstates = [aidQAR1.state, aidQAR2.state, aidQAR3.state]; + const states = rstates; + + const kargsMultisigAID: CreateIdentiferArgs = { + algo: signify.Algos.group, + isith: ['2/3', '1/2', '1/2'], + nsith: ['2/3', '1/2', '1/2'], + toad: kargsAID.toad, + wits: kargsAID.wits, + states: states, + rstates: rstates, + delpre: aidGEDA.prefix, + }; + + kargsMultisigAID.mhab = aidQAR1; + const multisigAIDOp1 = await createAIDMultisig( + clientQAR1, + aidQAR1, + [aidQAR2, aidQAR3], + 'QVI', + kargsMultisigAID, + true + ); + kargsMultisigAID.mhab = aidQAR2; + const multisigAIDOp2 = await createAIDMultisig( + clientQAR2, + aidQAR2, + [aidQAR1, aidQAR3], + 'QVI', + kargsMultisigAID + ); + kargsMultisigAID.mhab = aidQAR3; + const multisigAIDOp3 = await createAIDMultisig( + clientQAR3, + aidQAR3, + [aidQAR1, aidQAR2], + 'QVI', + kargsMultisigAID + ); + + const aidQVIPrefix = multisigAIDOp1.name.split('.')[1]; + assert.equal(multisigAIDOp2.name.split('.')[1], aidQVIPrefix); + assert.equal(multisigAIDOp3.name.split('.')[1], aidQVIPrefix); + + // GEDA anchors delegation with an interaction event. + const anchor = { + i: aidQVIPrefix, + s: '0', + d: aidQVIPrefix, + }; + const ixnOp1 = await interactMultisig( + clientGAR1, + aidGAR1, + [aidGAR2], + aidGEDA, + anchor, + true + ); + const ixnOp2 = await interactMultisig( + clientGAR2, + aidGAR2, + [aidGAR1], + aidGEDA, + anchor + ); + await Promise.all([ + waitOperation(clientGAR1, ixnOp1), + waitOperation(clientGAR2, ixnOp2), + ]); + + await waitAndMarkNotification(clientGAR1, '/multisig/ixn'); + + // QARs query the GEDA's key state + const queryOp1 = await clientQAR1 + .keyStates() + .query(aidGEDA.prefix, '1'); + const queryOp2 = await clientQAR2 + .keyStates() + .query(aidGEDA.prefix, '1'); + const queryOp3 = await clientQAR3 + .keyStates() + .query(aidGEDA.prefix, '1'); + + await Promise.all([ + waitOperation(clientQAR1, multisigAIDOp1), + waitOperation(clientQAR2, multisigAIDOp2), + waitOperation(clientQAR3, multisigAIDOp3), + waitOperation(clientQAR1, queryOp1), + waitOperation(clientQAR2, queryOp2), + waitOperation(clientQAR3, queryOp3), + ]); + + await waitAndMarkNotification(clientQAR1, '/multisig/icp'); + + aidQVIbyQAR1 = await clientQAR1.identifiers().get('QVI'); + aidQVIbyQAR2 = await clientQAR2.identifiers().get('QVI'); + aidQVIbyQAR3 = await clientQAR3.identifiers().get('QVI'); + } + assert.equal(aidQVIbyQAR1.prefix, aidQVIbyQAR2.prefix); + assert.equal(aidQVIbyQAR1.prefix, aidQVIbyQAR3.prefix); + assert.equal(aidQVIbyQAR1.name, aidQVIbyQAR2.name); + assert.equal(aidQVIbyQAR1.name, aidQVIbyQAR3.name); + const aidQVI = aidQVIbyQAR1; + + // Add endpoint role authorization for all QARs' agents. + // Skip if they have already been authorized. + let [oobiQVIbyQAR1, oobiQVIbyQAR2, oobiQVIbyQAR3] = await Promise.all([ + clientQAR1.oobis().get(aidQVI.name, 'agent'), + clientQAR2.oobis().get(aidQVI.name, 'agent'), + clientQAR3.oobis().get(aidQVI.name, 'agent'), + ]); + if ( + oobiQVIbyQAR1.oobis.length == 0 || + oobiQVIbyQAR2.oobis.length == 0 || + oobiQVIbyQAR3.oobis.length == 0 + ) { + const timestamp = createTimestamp(); + const opList1 = await addEndRoleMultisig( + clientQAR1, + aidQAR1, + [aidQAR2, aidQAR3], + aidQVI, + timestamp, + true + ); + const opList2 = await addEndRoleMultisig( + clientQAR2, + aidQAR2, + [aidQAR1, aidQAR3], + aidQVI, + timestamp + ); + const opList3 = await addEndRoleMultisig( + clientQAR3, + aidQAR3, + [aidQAR1, aidQAR2], + aidQVI, + timestamp + ); + + await Promise.all(opList1.map((op) => waitOperation(clientQAR1, op))); + await Promise.all(opList2.map((op) => waitOperation(clientQAR2, op))); + await Promise.all(opList3.map((op) => waitOperation(clientQAR3, op))); + + await waitAndMarkNotification(clientQAR1, '/multisig/rpy'); + await waitAndMarkNotification(clientQAR2, '/multisig/rpy'); + + [oobiQVIbyQAR1, oobiQVIbyQAR2, oobiQVIbyQAR3] = await Promise.all([ + clientQAR1.oobis().get(aidQVI.name, 'agent'), + clientQAR2.oobis().get(aidQVI.name, 'agent'), + clientQAR3.oobis().get(aidQVI.name, 'agent'), + ]); + } + assert.equal(oobiQVIbyQAR1.role, oobiQVIbyQAR2.role); + assert.equal(oobiQVIbyQAR1.role, oobiQVIbyQAR3.role); + assert.equal(oobiQVIbyQAR1.oobis[0], oobiQVIbyQAR2.oobis[0]); + assert.equal(oobiQVIbyQAR1.oobis[0], oobiQVIbyQAR3.oobis[0]); + + // GARs, LARs, ECR resolve QVI AID's OOBI + const oobiQVI = oobiQVIbyQAR1.oobis[0].split('/agent/')[0]; + await Promise.all([ + getOrCreateContact(clientGAR1, aidQVI.name, oobiQVI), + getOrCreateContact(clientGAR2, aidQVI.name, oobiQVI), + getOrCreateContact(clientLAR1, aidQVI.name, oobiQVI), + getOrCreateContact(clientLAR2, aidQVI.name, oobiQVI), + getOrCreateContact(clientLAR3, aidQVI.name, oobiQVI), + getOrCreateContact(clientECR, aidQVI.name, oobiQVI), + ]); + + // GARs creates a registry for GEDA. + // Skip if the registry has already been created. + let [gedaRegistrybyGAR1, gedaRegistrybyGAR2] = await Promise.all([ + clientGAR1.registries().list(aidGEDA.name), + clientGAR2.registries().list(aidGEDA.name), + ]); + if (gedaRegistrybyGAR1.length == 0 && gedaRegistrybyGAR2.length == 0) { + const nonce = randomNonce(); + const registryOp1 = await createRegistryMultisig( + clientGAR1, + aidGAR1, + [aidGAR2], + aidGEDA, + 'gedaRegistry', + nonce, + true + ); + const registryOp2 = await createRegistryMultisig( + clientGAR2, + aidGAR2, + [aidGAR1], + aidGEDA, + 'gedaRegistry', + nonce + ); + + await Promise.all([ + waitOperation(clientGAR1, registryOp1), + waitOperation(clientGAR2, registryOp2), + ]); + + await waitAndMarkNotification(clientGAR1, '/multisig/vcp'); + + [gedaRegistrybyGAR1, gedaRegistrybyGAR2] = await Promise.all([ + clientGAR1.registries().list(aidGEDA.name), + clientGAR2.registries().list(aidGEDA.name), + ]); + } + assert.equal(gedaRegistrybyGAR1[0].regk, gedaRegistrybyGAR2[0].regk); + assert.equal(gedaRegistrybyGAR1[0].name, gedaRegistrybyGAR2[0].name); + const gedaRegistry = gedaRegistrybyGAR1[0]; + + // GEDA issues a QVI vLEI credential to the QVI AID. + // Skip if the credential has already been issued. + let qviCredbyGAR1 = await getIssuedCredential( + clientGAR1, + aidGEDA, + aidQVI, + QVI_SCHEMA_SAID + ); + let qviCredbyGAR2 = await getIssuedCredential( + clientGAR2, + aidGEDA, + aidQVI, + QVI_SCHEMA_SAID + ); + if (!(qviCredbyGAR1 && qviCredbyGAR2)) { + const kargsSub: CredentialSubject = { + i: aidQVI.prefix, + dt: createTimestamp(), + ...qviData, + }; + const kargsIss: CredentialData = { + i: aidGEDA.prefix, + ri: gedaRegistry.regk, + s: QVI_SCHEMA_SAID, + a: kargsSub, + }; + const IssOp1 = await issueCredentialMultisig( + clientGAR1, + aidGAR1, + [aidGAR2], + aidGEDA.name, + kargsIss, + true + ); + const IssOp2 = await issueCredentialMultisig( + clientGAR2, + aidGAR2, + [aidGAR1], + aidGEDA.name, + kargsIss + ); + + await Promise.all([ + waitOperation(clientGAR1, IssOp1), + waitOperation(clientGAR2, IssOp2), + ]); + + await waitAndMarkNotification(clientGAR1, '/multisig/iss'); + + qviCredbyGAR1 = await getIssuedCredential( + clientGAR1, + aidGEDA, + aidQVI, + QVI_SCHEMA_SAID + ); + qviCredbyGAR2 = await getIssuedCredential( + clientGAR2, + aidGEDA, + aidQVI, + QVI_SCHEMA_SAID + ); + + const grantTime = createTimestamp(); + await grantMultisig( + clientGAR1, + aidGAR1, + [aidGAR2], + aidGEDA, + aidQVI, + qviCredbyGAR1, + grantTime, + true + ); + await grantMultisig( + clientGAR2, + aidGAR2, + [aidGAR1], + aidGEDA, + aidQVI, + qviCredbyGAR2, + grantTime + ); + + await waitAndMarkNotification(clientGAR1, '/multisig/exn'); + } + assert.equal(qviCredbyGAR1.sad.d, qviCredbyGAR2.sad.d); + assert.equal(qviCredbyGAR1.sad.s, QVI_SCHEMA_SAID); + assert.equal(qviCredbyGAR1.sad.i, aidGEDA.prefix); + assert.equal(qviCredbyGAR1.sad.a.i, aidQVI.prefix); + assert.equal(qviCredbyGAR1.status.s, '0'); + assert(qviCredbyGAR1.atc !== undefined); + const qviCred = qviCredbyGAR1; + console.log( + 'GEDA has issued a QVI vLEI credential with SAID:', + qviCred.sad.d + ); + + // GEDA and QVI exchange grant and admit messages. + // Skip if QVI has already received the credential. + let qviCredbyQAR1 = await getReceivedCredential(clientQAR1, qviCred.sad.d); + let qviCredbyQAR2 = await getReceivedCredential(clientQAR2, qviCred.sad.d); + let qviCredbyQAR3 = await getReceivedCredential(clientQAR3, qviCred.sad.d); + if (!(qviCredbyQAR1 && qviCredbyQAR2 && qviCredbyQAR3)) { + const admitTime = createTimestamp(); + await admitMultisig( + clientQAR1, + aidQAR1, + [aidQAR2, aidQAR3], + aidQVI, + aidGEDA, + admitTime + ); + await admitMultisig( + clientQAR2, + aidQAR2, + [aidQAR1, aidQAR3], + aidQVI, + aidGEDA, + admitTime + ); + await admitMultisig( + clientQAR3, + aidQAR3, + [aidQAR1, aidQAR2], + aidQVI, + aidGEDA, + admitTime + ); + await waitAndMarkNotification(clientGAR1, '/exn/ipex/admit'); + await waitAndMarkNotification(clientGAR2, '/exn/ipex/admit'); + await waitAndMarkNotification(clientQAR1, '/multisig/exn'); + await waitAndMarkNotification(clientQAR2, '/multisig/exn'); + await waitAndMarkNotification(clientQAR3, '/multisig/exn'); + await waitAndMarkNotification(clientQAR1, '/exn/ipex/admit'); + await waitAndMarkNotification(clientQAR2, '/exn/ipex/admit'); + await waitAndMarkNotification(clientQAR3, '/exn/ipex/admit'); + + qviCredbyQAR1 = await waitForCredential(clientQAR1, qviCred.sad.d); + qviCredbyQAR2 = await waitForCredential(clientQAR2, qviCred.sad.d); + qviCredbyQAR3 = await waitForCredential(clientQAR3, qviCred.sad.d); + } + assert.equal(qviCred.sad.d, qviCredbyQAR1.sad.d); + assert.equal(qviCred.sad.d, qviCredbyQAR2.sad.d); + assert.equal(qviCred.sad.d, qviCredbyQAR3.sad.d); + + // Create a multisig AID for the LE. + // Skip if a LE AID has already been incepted. + let aidLEbyLAR1, aidLEbyLAR2, aidLEbyLAR3: Aid; + try { + aidLEbyLAR1 = await clientLAR1.identifiers().get('LE'); + aidLEbyLAR2 = await clientLAR2.identifiers().get('LE'); + aidLEbyLAR3 = await clientLAR3.identifiers().get('LE'); + } catch { + const rstates = [aidLAR1.state, aidLAR2.state, aidLAR3.state]; + const states = rstates; + + const kargsMultisigAID: CreateIdentiferArgs = { + algo: signify.Algos.group, + isith: ['2/3', '1/2', '1/2'], + nsith: ['2/3', '1/2', '1/2'], + toad: kargsAID.toad, + wits: kargsAID.wits, + states: states, + rstates: rstates, + }; + + kargsMultisigAID.mhab = aidLAR1; + const multisigAIDOp1 = await createAIDMultisig( + clientLAR1, + aidLAR1, + [aidLAR2, aidLAR3], + 'LE', + kargsMultisigAID, + true + ); + kargsMultisigAID.mhab = aidLAR2; + const multisigAIDOp2 = await createAIDMultisig( + clientLAR2, + aidLAR2, + [aidLAR1, aidLAR3], + 'LE', + kargsMultisigAID + ); + kargsMultisigAID.mhab = aidLAR3; + const multisigAIDOp3 = await createAIDMultisig( + clientLAR3, + aidLAR3, + [aidLAR1, aidLAR2], + 'LE', + kargsMultisigAID + ); + + await Promise.all([ + waitOperation(clientLAR1, multisigAIDOp1), + waitOperation(clientLAR2, multisigAIDOp2), + waitOperation(clientLAR3, multisigAIDOp3), + ]); + + await waitAndMarkNotification(clientLAR1, '/multisig/icp'); + + aidLEbyLAR1 = await clientLAR1.identifiers().get('LE'); + aidLEbyLAR2 = await clientLAR2.identifiers().get('LE'); + aidLEbyLAR3 = await clientLAR3.identifiers().get('LE'); + } + assert.equal(aidLEbyLAR1.prefix, aidLEbyLAR2.prefix); + assert.equal(aidLEbyLAR1.prefix, aidLEbyLAR3.prefix); + assert.equal(aidLEbyLAR1.name, aidLEbyLAR2.name); + assert.equal(aidLEbyLAR1.name, aidLEbyLAR3.name); + const aidLE = aidLEbyLAR1; + + // Add endpoint role authorization for all LARs' agents. + // Skip if they have already been authorized. + let [oobiLEbyLAR1, oobiLEbyLAR2, oobiLEbyLAR3] = await Promise.all([ + clientLAR1.oobis().get(aidLE.name, 'agent'), + clientLAR2.oobis().get(aidLE.name, 'agent'), + clientLAR3.oobis().get(aidLE.name, 'agent'), + ]); + if ( + oobiLEbyLAR1.oobis.length == 0 || + oobiLEbyLAR2.oobis.length == 0 || + oobiLEbyLAR3.oobis.length == 0 + ) { + const timestamp = createTimestamp(); + const opList1 = await addEndRoleMultisig( + clientLAR1, + aidLAR1, + [aidLAR2, aidLAR3], + aidLE, + timestamp, + true + ); + const opList2 = await addEndRoleMultisig( + clientLAR2, + aidLAR2, + [aidLAR1, aidLAR3], + aidLE, + timestamp + ); + const opList3 = await addEndRoleMultisig( + clientLAR3, + aidLAR3, + [aidLAR1, aidLAR2], + aidLE, + timestamp + ); + + await Promise.all(opList1.map((op) => waitOperation(clientLAR1, op))); + await Promise.all(opList2.map((op) => waitOperation(clientLAR2, op))); + await Promise.all(opList3.map((op) => waitOperation(clientLAR3, op))); + + await waitAndMarkNotification(clientLAR1, '/multisig/rpy'); + await waitAndMarkNotification(clientLAR2, '/multisig/rpy'); + + [oobiLEbyLAR1, oobiLEbyLAR2, oobiLEbyLAR3] = await Promise.all([ + clientLAR1.oobis().get(aidLE.name, 'agent'), + clientLAR2.oobis().get(aidLE.name, 'agent'), + clientLAR3.oobis().get(aidLE.name, 'agent'), + ]); + } + assert.equal(oobiLEbyLAR1.role, oobiLEbyLAR2.role); + assert.equal(oobiLEbyLAR1.role, oobiLEbyLAR3.role); + assert.equal(oobiLEbyLAR1.oobis[0], oobiLEbyLAR2.oobis[0]); + assert.equal(oobiLEbyLAR1.oobis[0], oobiLEbyLAR3.oobis[0]); + + // QARs, ECR resolve LE AID's OOBI + const oobiLE = oobiLEbyLAR1.oobis[0].split('/agent/')[0]; + await Promise.all([ + getOrCreateContact(clientQAR1, aidLE.name, oobiLE), + getOrCreateContact(clientQAR2, aidLE.name, oobiLE), + getOrCreateContact(clientQAR3, aidLE.name, oobiLE), + getOrCreateContact(clientECR, aidLE.name, oobiLE), + ]); + + // QARs creates a registry for QVI AID. + // Skip if the registry has already been created. + let [qviRegistrybyQAR1, qviRegistrybyQAR2, qviRegistrybyQAR3] = + await Promise.all([ + clientQAR1.registries().list(aidQVI.name), + clientQAR2.registries().list(aidQVI.name), + clientQAR3.registries().list(aidQVI.name), + ]); + if ( + qviRegistrybyQAR1.length == 0 && + qviRegistrybyQAR2.length == 0 && + qviRegistrybyQAR3.length == 0 + ) { + const nonce = randomNonce(); + const registryOp1 = await createRegistryMultisig( + clientQAR1, + aidQAR1, + [aidQAR2, aidQAR3], + aidQVI, + 'qviRegistry', + nonce, + true + ); + const registryOp2 = await createRegistryMultisig( + clientQAR2, + aidQAR2, + [aidQAR1, aidQAR3], + aidQVI, + 'qviRegistry', + nonce + ); + const registryOp3 = await createRegistryMultisig( + clientQAR3, + aidQAR3, + [aidQAR1, aidQAR2], + aidQVI, + 'qviRegistry', + nonce + ); + + await Promise.all([ + waitOperation(clientQAR1, registryOp1), + waitOperation(clientQAR2, registryOp2), + waitOperation(clientQAR3, registryOp3), + ]); + + await waitAndMarkNotification(clientQAR1, '/multisig/vcp'); + + [qviRegistrybyQAR1, qviRegistrybyQAR2, qviRegistrybyQAR3] = + await Promise.all([ + clientQAR1.registries().list(aidQVI.name), + clientQAR2.registries().list(aidQVI.name), + clientQAR3.registries().list(aidQVI.name), + ]); + } + assert.equal(qviRegistrybyQAR1[0].regk, qviRegistrybyQAR2[0].regk); + assert.equal(qviRegistrybyQAR1[0].regk, qviRegistrybyQAR3[0].regk); + assert.equal(qviRegistrybyQAR1[0].name, qviRegistrybyQAR2[0].name); + assert.equal(qviRegistrybyQAR1[0].name, qviRegistrybyQAR3[0].name); + const qviRegistry = qviRegistrybyQAR1[0]; + + // QVI issues a LE vLEI credential to the LE. + // Skip if the credential has already been issued. + let leCredbyQAR1 = await getIssuedCredential( + clientQAR1, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + let leCredbyQAR2 = await getIssuedCredential( + clientQAR2, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + let leCredbyQAR3 = await getIssuedCredential( + clientQAR3, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + if (!(leCredbyQAR1 && leCredbyQAR2 && leCredbyQAR3)) { + const leCredSource = Saider.saidify({ + d: '', + qvi: { + n: qviCred.sad.d, + s: qviCred.sad.s, + }, + })[1]; + + const kargsSub: CredentialSubject = { + i: aidLE.prefix, + dt: createTimestamp(), + ...leData, + }; + const kargsIss: CredentialData = { + i: aidQVI.prefix, + ri: qviRegistry.regk, + s: LE_SCHEMA_SAID, + a: kargsSub, + e: leCredSource, + r: LE_RULES, + }; + const IssOp1 = await issueCredentialMultisig( + clientQAR1, + aidQAR1, + [aidQAR2, aidQAR3], + aidQVI.name, + kargsIss, + true + ); + const IssOp2 = await issueCredentialMultisig( + clientQAR2, + aidQAR2, + [aidQAR1, aidQAR3], + aidQVI.name, + kargsIss + ); + const IssOp3 = await issueCredentialMultisig( + clientQAR3, + aidQAR3, + [aidQAR1, aidQAR2], + aidQVI.name, + kargsIss + ); + + await Promise.all([ + waitOperation(clientQAR1, IssOp1), + waitOperation(clientQAR2, IssOp2), + waitOperation(clientQAR3, IssOp3), + ]); + + await waitAndMarkNotification(clientQAR1, '/multisig/iss'); + + leCredbyQAR1 = await getIssuedCredential( + clientQAR1, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + leCredbyQAR2 = await getIssuedCredential( + clientQAR2, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + leCredbyQAR3 = await getIssuedCredential( + clientQAR3, + aidQVI, + aidLE, + LE_SCHEMA_SAID + ); + + const grantTime = createTimestamp(); + await grantMultisig( + clientQAR1, + aidQAR1, + [aidQAR2, aidQAR3], + aidQVI, + aidLE, + leCredbyQAR1, + grantTime, + true + ); + await grantMultisig( + clientQAR2, + aidQAR2, + [aidQAR1, aidQAR3], + aidQVI, + aidLE, + leCredbyQAR2, + grantTime + ); + await grantMultisig( + clientQAR3, + aidQAR3, + [aidQAR1, aidQAR2], + aidQVI, + aidLE, + leCredbyQAR3, + grantTime + ); + + await waitAndMarkNotification(clientQAR1, '/multisig/exn'); + } + assert.equal(leCredbyQAR1.sad.d, leCredbyQAR2.sad.d); + assert.equal(leCredbyQAR1.sad.d, leCredbyQAR3.sad.d); + assert.equal(leCredbyQAR1.sad.s, LE_SCHEMA_SAID); + assert.equal(leCredbyQAR1.sad.i, aidQVI.prefix); + assert.equal(leCredbyQAR1.sad.a.i, aidLE.prefix); + assert.equal(leCredbyQAR1.status.s, '0'); + assert(leCredbyQAR1.atc !== undefined); + const leCred = leCredbyQAR1; + console.log('QVI has issued a LE vLEI credential with SAID:', leCred.sad.d); + + // QVI and LE exchange grant and admit messages. + // Skip if LE has already received the credential. + let leCredbyLAR1 = await getReceivedCredential(clientLAR1, leCred.sad.d); + let leCredbyLAR2 = await getReceivedCredential(clientLAR2, leCred.sad.d); + let leCredbyLAR3 = await getReceivedCredential(clientLAR3, leCred.sad.d); + if (!(leCredbyLAR1 && leCredbyLAR2 && leCredbyLAR3)) { + const admitTime = createTimestamp(); + await admitMultisig( + clientLAR1, + aidLAR1, + [aidLAR2, aidLAR3], + aidLE, + aidQVI, + admitTime + ); + await admitMultisig( + clientLAR2, + aidLAR2, + [aidLAR1, aidLAR3], + aidLE, + aidQVI, + admitTime + ); + await admitMultisig( + clientLAR3, + aidLAR3, + [aidLAR1, aidLAR2], + aidLE, + aidQVI, + admitTime + ); + await waitAndMarkNotification(clientQAR1, '/exn/ipex/admit'); + await waitAndMarkNotification(clientQAR2, '/exn/ipex/admit'); + await waitAndMarkNotification(clientQAR3, '/exn/ipex/admit'); + await waitAndMarkNotification(clientLAR1, '/multisig/exn'); + await waitAndMarkNotification(clientLAR2, '/multisig/exn'); + await waitAndMarkNotification(clientLAR3, '/multisig/exn'); + await waitAndMarkNotification(clientLAR1, '/exn/ipex/admit'); + await waitAndMarkNotification(clientLAR2, '/exn/ipex/admit'); + await waitAndMarkNotification(clientLAR3, '/exn/ipex/admit'); + + leCredbyLAR1 = await waitForCredential(clientLAR1, leCred.sad.d); + leCredbyLAR2 = await waitForCredential(clientLAR2, leCred.sad.d); + leCredbyLAR3 = await waitForCredential(clientLAR3, leCred.sad.d); + } + assert.equal(leCred.sad.d, leCredbyLAR1.sad.d); + assert.equal(leCred.sad.d, leCredbyLAR2.sad.d); + assert.equal(leCred.sad.d, leCredbyLAR3.sad.d); + + // LARs creates a registry for LE AID. + // Skip if the registry has already been created. + let [leRegistrybyLAR1, leRegistrybyLAR2, leRegistrybyLAR3] = + await Promise.all([ + clientLAR1.registries().list(aidLE.name), + clientLAR2.registries().list(aidLE.name), + clientLAR3.registries().list(aidLE.name), + ]); + if ( + leRegistrybyLAR1.length == 0 && + leRegistrybyLAR2.length == 0 && + leRegistrybyLAR3.length == 0 + ) { + const nonce = randomNonce(); + const registryOp1 = await createRegistryMultisig( + clientLAR1, + aidLAR1, + [aidLAR2, aidLAR3], + aidLE, + 'leRegistry', + nonce, + true + ); + const registryOp2 = await createRegistryMultisig( + clientLAR2, + aidLAR2, + [aidLAR1, aidLAR3], + aidLE, + 'leRegistry', + nonce + ); + const registryOp3 = await createRegistryMultisig( + clientLAR3, + aidLAR3, + [aidLAR1, aidLAR2], + aidLE, + 'leRegistry', + nonce + ); + + await Promise.all([ + waitOperation(clientLAR1, registryOp1), + waitOperation(clientLAR2, registryOp2), + waitOperation(clientLAR3, registryOp3), + ]); + + await waitAndMarkNotification(clientLAR1, '/multisig/vcp'); + + [leRegistrybyLAR1, leRegistrybyLAR2, leRegistrybyLAR3] = + await Promise.all([ + clientLAR1.registries().list(aidLE.name), + clientLAR2.registries().list(aidLE.name), + clientLAR3.registries().list(aidLE.name), + ]); + } + assert.equal(leRegistrybyLAR1[0].regk, leRegistrybyLAR2[0].regk); + assert.equal(leRegistrybyLAR1[0].regk, leRegistrybyLAR3[0].regk); + assert.equal(leRegistrybyLAR1[0].name, leRegistrybyLAR2[0].name); + assert.equal(leRegistrybyLAR1[0].name, leRegistrybyLAR3[0].name); + const leRegistry = leRegistrybyLAR1[0]; + + // LE issues a ECR vLEI credential to the ECR Person. + // Skip if the credential has already been issued. + let ecrCredbyLAR1 = await getIssuedCredential( + clientLAR1, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + let ecrCredbyLAR2 = await getIssuedCredential( + clientLAR2, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + let ecrCredbyLAR3 = await getIssuedCredential( + clientLAR3, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + if (!(ecrCredbyLAR1 && ecrCredbyLAR2 && ecrCredbyLAR3)) { + console.log('Issuing ECR vLEI Credential from LE'); + const ecrCredSource = Saider.saidify({ + d: '', + le: { + n: leCred.sad.d, + s: leCred.sad.s, + }, + })[1]; + + const kargsSub: CredentialSubject = { + i: aidECR.prefix, + dt: createTimestamp(), + u: new Salter({}).qb64, + ...ecrData, + }; + const kargsIss: CredentialData = { + u: new Salter({}).qb64, + i: aidLE.prefix, + ri: leRegistry.regk, + s: ECR_SCHEMA_SAID, + a: kargsSub, + e: ecrCredSource, + r: ECR_RULES, + }; + + const IssOp1 = await issueCredentialMultisig( + clientLAR1, + aidLAR1, + [aidLAR2, aidLAR3], + aidLE.name, + kargsIss, + true + ); + const IssOp2 = await issueCredentialMultisig( + clientLAR2, + aidLAR2, + [aidLAR1, aidLAR3], + aidLE.name, + kargsIss + ); + const IssOp3 = await issueCredentialMultisig( + clientLAR3, + aidLAR3, + [aidLAR1, aidLAR2], + aidLE.name, + kargsIss + ); + + await Promise.all([ + waitOperation(clientLAR1, IssOp1), + waitOperation(clientLAR2, IssOp2), + waitOperation(clientLAR3, IssOp3), + ]); + + await waitAndMarkNotification(clientLAR1, '/multisig/iss'); + + ecrCredbyLAR1 = await getIssuedCredential( + clientLAR1, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + ecrCredbyLAR2 = await getIssuedCredential( + clientLAR2, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + ecrCredbyLAR3 = await getIssuedCredential( + clientLAR3, + aidLE, + aidECR, + ECR_SCHEMA_SAID + ); + + const grantTime = createTimestamp(); + await grantMultisig( + clientLAR1, + aidLAR1, + [aidLAR2, aidLAR3], + aidLE, + aidECR, + ecrCredbyLAR1, + grantTime, + true + ); + await grantMultisig( + clientLAR2, + aidLAR2, + [aidLAR1, aidLAR3], + aidLE, + aidECR, + ecrCredbyLAR2, + grantTime + ); + await grantMultisig( + clientLAR3, + aidLAR3, + [aidLAR1, aidLAR2], + aidLE, + aidECR, + ecrCredbyLAR3, + grantTime + ); + + await waitAndMarkNotification(clientLAR1, '/multisig/exn'); + } + assert.equal(ecrCredbyLAR1.sad.d, ecrCredbyLAR2.sad.d); + assert.equal(ecrCredbyLAR1.sad.d, ecrCredbyLAR3.sad.d); + assert.equal(ecrCredbyLAR1.sad.s, ECR_SCHEMA_SAID); + assert.equal(ecrCredbyLAR1.sad.i, aidLE.prefix); + assert.equal(ecrCredbyLAR1.sad.a.i, aidECR.prefix); + assert.equal(ecrCredbyLAR1.status.s, '0'); + assert(ecrCredbyLAR1.atc !== undefined); + const ecrCred = ecrCredbyLAR1; + console.log( + 'LE has issued an ECR vLEI credential with SAID:', + ecrCred.sad.d + ); + + // LE and ECR Person exchange grant and admit messages. + // Skip if ECR Person has already received the credential. + let ecrCredbyECR = await getReceivedCredential(clientECR, ecrCred.sad.d); + if (!ecrCredbyECR) { + await admitSinglesig(clientECR, aidECR, aidLE); + await waitAndMarkNotification(clientLAR1, '/exn/ipex/admit'); + await waitAndMarkNotification(clientLAR2, '/exn/ipex/admit'); + await waitAndMarkNotification(clientLAR3, '/exn/ipex/admit'); + + ecrCredbyECR = await waitForCredential(clientECR, ecrCred.sad.d); + } + assert.equal(ecrCred.sad.d, ecrCredbyECR.sad.d); +}, 360000); + +function createTimestamp() { + return new Date().toISOString().replace('Z', '000+00:00'); +} + +async function getOrCreateAID( + client: SignifyClient, + name: string, + kargs: CreateIdentiferArgs +): Promise { + let aid: Aid; + try { + aid = await client.identifiers().get(name); + } catch { + const result: EventResult = await client + .identifiers() + .create(name, kargs); + + await waitOperation(client, await result.op()); + aid = await client.identifiers().get(name); + + await client + .identifiers() + .addEndRole(name, 'agent', client!.agent!.pre); + console.log(name, 'AID:', aid.prefix); + } + return aid; +} + +async function createAIDMultisig( + client: SignifyClient, + aid: Aid, + otherMembersAIDs: Aid[], + groupName: string, + kargs: CreateIdentiferArgs, + isInitiator: boolean = false +) { + if (!isInitiator) await waitAndMarkNotification(client, '/multisig/icp'); + + const icpResult = await client.identifiers().create(groupName, kargs); + const op = await icpResult.op(); + + const serder = icpResult.serder; + const sigs = icpResult.sigs; + const sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(serder, sigers)); + const atc = ims.substring(serder.size); + const embeds = { + icp: [serder, atc], + }; + const smids = kargs.states?.map((state) => state['i']); + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + 'multisig', + aid, + '/multisig/icp', + { gid: serder.pre, smids: smids, rmids: smids }, + embeds, + recp + ); + + return op; +} + +async function interactMultisig( + client: SignifyClient, + aid: Aid, + otherMembersAIDs: Aid[], + multisigAID: Aid, + anchor: { i: string; s: string; d: string }, + isInitiator: boolean = false +) { + if (!isInitiator) await waitAndMarkNotification(client, '/multisig/ixn'); + + const ixnResult = await client + .identifiers() + .interact(multisigAID.name, anchor); + const op = await ixnResult.op(); + const serder = ixnResult.serder; + const sigs = ixnResult.sigs; + const sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(serder, sigers)); + const atc = ims.substring(serder.size); + const xembeds = { + ixn: [serder, atc], + }; + const smids = [aid.prefix, ...otherMembersAIDs.map((aid) => aid.prefix)]; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + 'multisig', + aid, + '/multisig/ixn', + { gid: serder.pre, smids: smids, rmids: smids }, + xembeds, + recp + ); + + return op; +} + +async function addEndRoleMultisig( + client: SignifyClient, + aid: Aid, + otherMembersAIDs: Aid[], + multisigAID: Aid, + timestamp: string, + isInitiator: boolean = false +) { + if (!isInitiator) await waitAndMarkNotification(client, '/multisig/rpy'); + + const opList: any[] = []; + const members = await client.identifiers().members(multisigAID.name); + const signings = members['signing']; + + for (const signing of signings) { + const eid = Object.keys(signing.ends.agent)[0]; + const endRoleResult = await client + .identifiers() + .addEndRole(multisigAID.name, 'agent', eid, timestamp); + const op = await endRoleResult.op(); + opList.push(op); + + const rpy = endRoleResult.serder; + const sigs = endRoleResult.sigs; + const ghabState1 = multisigAID.state; + const seal = [ + 'SealEvent', + { + i: multisigAID.prefix, + s: ghabState1['ee']['s'], + d: ghabState1['ee']['d'], + }, + ]; + const sigers = sigs.map( + (sig: string) => new signify.Siger({ qb64: sig }) + ); + const roleims = signify.d( + signify.messagize(rpy, sigers, seal, undefined, undefined, false) + ); + const atc = roleims.substring(rpy.size); + const roleembeds = { + rpy: [rpy, atc], + }; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + await client + .exchanges() + .send( + aid.name, + 'multisig', + aid, + '/multisig/rpy', + { gid: multisigAID.prefix }, + roleembeds, + recp + ); + } + + return opList; +} + +async function createRegistryMultisig( + client: SignifyClient, + aid: Aid, + otherMembersAIDs: Aid[], + multisigAID: Aid, + registryName: string, + nonce: string, + isInitiator: boolean = false +) { + if (!isInitiator) await waitAndMarkNotification(client, '/multisig/vcp'); + + const vcpResult = await client.registries().create({ + name: multisigAID.name, + registryName: registryName, + nonce: nonce, + }); + const op = await vcpResult.op(); + + const serder = vcpResult.regser; + const anc = vcpResult.serder; + const sigs = vcpResult.sigs; + const sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(anc, sigers)); + const atc = ims.substring(anc.size); + const regbeds = { + vcp: [serder, ''], + anc: [anc, atc], + }; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + 'registry', + aid, + '/multisig/vcp', + { gid: multisigAID.prefix }, + regbeds, + recp + ); + + return op; +} + +async function getIssuedCredential( + issuerClient: SignifyClient, + issuerAID: Aid, + recipientAID: Aid, + schemaSAID: string +) { + const credentialList = await issuerClient.credentials().list({ + filter: { + '-i': issuerAID.prefix, + '-s': schemaSAID, + '-a-i': recipientAID.prefix, + }, + }); + assert(credentialList.length <= 1); + return credentialList[0]; +} + +async function issueCredentialMultisig( + client: SignifyClient, + aid: Aid, + otherMembersAIDs: Aid[], + multisigAIDName: string, + kargsIss: CredentialData, + isInitiator: boolean = false +) { + if (!isInitiator) await waitAndMarkNotification(client, '/multisig/iss'); + + const credResult = await client + .credentials() + .issue(multisigAIDName, kargsIss); + const op = credResult.op; + + const multisigAID = await client.identifiers().get(multisigAIDName); + const keeper = client.manager!.get(multisigAID); + const sigs = await keeper.sign(signify.b(credResult.anc.raw)); + const sigers = sigs.map((sig: string) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(credResult.anc, sigers)); + const atc = ims.substring(credResult.anc.size); + const embeds = { + acdc: [credResult.acdc, ''], + iss: [credResult.iss, ''], + anc: [credResult.anc, atc], + }; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + 'multisig', + aid, + '/multisig/iss', + { gid: multisigAID.prefix }, + embeds, + recp + ); + + return op; +} + +async function grantMultisig( + client: SignifyClient, + aid: Aid, + otherMembersAIDs: Aid[], + multisigAID: Aid, + recipientAID: Aid, + credential: any, + timestamp: string, + isInitiator: boolean = false +) { + if (!isInitiator) await waitAndMarkNotification(client, '/multisig/exn'); + + const [grant, sigs, end] = await client.ipex().grant({ + senderName: multisigAID.name, + acdc: new Serder(credential.sad), + anc: new Serder(credential.anc), + iss: new Serder(credential.iss), + recipient: recipientAID.prefix, + datetime: timestamp, + }); + + await client + .ipex() + .submitGrant(multisigAID.name, grant, sigs, end, [recipientAID.prefix]); + + const mstate = multisigAID.state; + const seal = [ + 'SealEvent', + { i: multisigAID.prefix, s: mstate['ee']['s'], d: mstate['ee']['d'] }, + ]; + const sigers = sigs.map((sig) => new signify.Siger({ qb64: sig })); + const gims = signify.d(signify.messagize(grant, sigers, seal)); + let atc = gims.substring(grant.size); + atc += end; + const gembeds = { + exn: [grant, atc], + }; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + 'multisig', + aid, + '/multisig/exn', + { gid: multisigAID.prefix }, + gembeds, + recp + ); +} + +async function admitMultisig( + client: SignifyClient, + aid: Aid, + otherMembersAIDs: Aid[], + multisigAID: Aid, + recipientAID: Aid, + timestamp: string + // numGrantMsgs: number +) { + const grantMsgSaid = await waitAndMarkNotification( + client, + '/exn/ipex/grant' + ); + + const [admit, sigs, end] = await client + .ipex() + .admit(multisigAID.name, '', grantMsgSaid, timestamp); + + await client + .ipex() + .submitAdmit(multisigAID.name, admit, sigs, end, [recipientAID.prefix]); + + const mstate = multisigAID.state; + const seal = [ + 'SealEvent', + { i: multisigAID.prefix, s: mstate['ee']['s'], d: mstate['ee']['d'] }, + ]; + const sigers = sigs.map((sig: string) => new signify.Siger({ qb64: sig })); + const ims = signify.d(signify.messagize(admit, sigers, seal)); + let atc = ims.substring(admit.size); + atc += end; + const gembeds = { + exn: [admit, atc], + }; + const recp = otherMembersAIDs.map((aid) => aid.prefix); + + await client + .exchanges() + .send( + aid.name, + 'multisig', + aid, + '/multisig/exn', + { gid: multisigAID.prefix }, + gembeds, + recp + ); +} + +async function admitSinglesig( + client: SignifyClient, + aid: Aid, + recipientAid: Aid +) { + const grantMsgSaid = await waitAndMarkNotification( + client, + '/exn/ipex/grant' + ); + + const [admit, sigs, aend] = await client + .ipex() + .admit(aid.name, '', grantMsgSaid); + + await client + .ipex() + .submitAdmit(aid.name, admit, sigs, aend, [recipientAid.prefix]); +} + +async function waitAndMarkNotification(client: SignifyClient, route: string) { + const notes = await waitForNotifications(client, route); + + await Promise.all( + notes.map(async (note) => { + await client.notifications().mark(note.i); + }) + ); + + return notes[notes.length - 1]?.a.d ?? ''; +} + +async function getReceivedCredential( + client: SignifyClient, + credId: string +): Promise { + const credentialList = await client.credentials().list({ + filter: { + '-d': credId, + }, + }); + return credentialList[0]; +} + +async function waitForCredential( + client: SignifyClient, + credSAID: string, + MAX_RETRIES: number = 10 +) { + let retryCount = 0; + while (retryCount < MAX_RETRIES) { + const cred = await getReceivedCredential(client, credSAID); + if (cred) return cred; + + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log(` retry-${retryCount}: No credentials yet...`); + retryCount = retryCount + 1; + } + throw Error('Credential SAID: ' + credSAID + ' has not been received'); +} diff --git a/examples/integration-scripts/singlesig-vlei-issuance.test.ts b/examples/integration-scripts/singlesig-vlei-issuance.test.ts index 62d7200b..6aacbc51 100644 --- a/examples/integration-scripts/singlesig-vlei-issuance.test.ts +++ b/examples/integration-scripts/singlesig-vlei-issuance.test.ts @@ -496,8 +496,6 @@ async function getOrIssueCredential( const credentialList = await issuerClient.credentials().list(); if (credentialList.length > 0) { - for (let cred of credentialList) { - } const credential = credentialList.find( (cred: any) => cred.sad.s === schema && From a6949da5e68a228ec5ca6f0e30077be9c644f74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lenksj=C3=B6?= <5889538+lenkan@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:47:33 +0200 Subject: [PATCH 12/18] use named imports from mathjs to reduce bundle size (#235) --- src/exports.ts | 1 - src/keri/core/tholder.ts | 11 +++++----- src/math.ts | 3 --- test/core/tholder.test.ts | 45 +++++++++++++++++---------------------- 4 files changed, 25 insertions(+), 35 deletions(-) delete mode 100644 src/math.ts diff --git a/src/exports.ts b/src/exports.ts index 7561737f..839f508a 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,5 +1,4 @@ export * from './ready'; -export * from './math'; export * from './keri/app/habery'; export * from './keri/app/controller'; diff --git a/src/keri/core/tholder.ts b/src/keri/core/tholder.ts index 4221df25..29234d5e 100644 --- a/src/keri/core/tholder.ts +++ b/src/keri/core/tholder.ts @@ -1,7 +1,6 @@ import { BexDex, Matter, NumDex } from './matter'; import { CesrNumber } from './number'; -import { Fraction } from 'mathjs'; -import { math } from '../../index'; +import { Fraction, format, sum, fraction } from 'mathjs'; export class Tholder { private _weighted: boolean = false; @@ -45,9 +44,9 @@ export class Tholder { let sith = this.thold.map((clause: Fraction[]) => { return clause.map((c) => { if (0 < Number(c) && Number(c) < 1) { - return math.format(c, { fraction: 'ratio' }); + return format(c, { fraction: 'ratio' }); } else { - return math.format(c, { fraction: 'decimal' }); + return format(c, { fraction: 'decimal' }); } }); }); @@ -159,7 +158,7 @@ export class Tholder { private _processWeighted(thold: Array>) { for (const clause of thold) { - if (Number(math.sum(clause)) < 1) { + if (Number(sum(clause)) < 1) { throw new Error( 'Invalid sith clause: ' + thold + @@ -178,7 +177,7 @@ export class Tholder { } private weight(w: string): Fraction { - return math.fraction(w); + return fraction(w); } private _satisfy_numeric(indices: any[]) { diff --git a/src/math.ts b/src/math.ts deleted file mode 100644 index d5e60e2f..00000000 --- a/src/math.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { create, all } from 'mathjs'; -const config = {}; -export const math = create(all, config); diff --git a/test/core/tholder.test.ts b/test/core/tholder.test.ts index 5efced45..765242b5 100644 --- a/test/core/tholder.test.ts +++ b/test/core/tholder.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'assert'; +import { fraction } from 'mathjs'; import { Tholder } from '../../src/keri/core/tholder'; -import { math } from '../../src'; describe('THolder', () => { it('should hold thresholds', async () => { @@ -66,11 +66,11 @@ describe('THolder', () => { assert.equal(tholder.size, 5); assert.deepStrictEqual(tholder.thold, [ [ - math.fraction('1/2'), - math.fraction('1/2'), - math.fraction('1/4'), - math.fraction('1/4'), - math.fraction('1/4'), + fraction('1/2'), + fraction('1/2'), + fraction('1/4'), + fraction('1/4'), + fraction('1/4'), ], ]); assert.equal(tholder.satisfy([0, 1]), true); @@ -96,13 +96,8 @@ describe('THolder', () => { ['1/3', '1/3', '1/3', '1/3'], ]); assert.deepStrictEqual(tholder.thold, [ - [math.fraction(1, 2), math.fraction(1, 2), math.fraction(1, 2)], - [ - math.fraction(1, 3), - math.fraction(1, 3), - math.fraction(1, 3), - math.fraction(1, 3), - ], + [fraction(1, 2), fraction(1, 2), fraction(1, 2)], + [fraction(1, 3), fraction(1, 3), fraction(1, 3), fraction(1, 3)], ]); assert.equal(tholder.satisfy([0, 2, 3, 5, 6]), true); assert.equal(tholder.satisfy([1, 2, 3, 4, 5]), true); @@ -122,13 +117,13 @@ describe('THolder', () => { ]); assert.deepStrictEqual(tholder.thold, [ [ - math.fraction(1, 2), - math.fraction(1, 2), - math.fraction(1, 4), - math.fraction(1, 4), - math.fraction(1, 4), + fraction(1, 2), + fraction(1, 2), + fraction(1, 4), + fraction(1, 4), + fraction(1, 4), ], - [math.fraction(1, 1), math.fraction(1, 1)], + [fraction(1, 1), fraction(1, 1)], ]); assert.equal(tholder.satisfy([1, 2, 3, 5]), true); assert.equal(tholder.satisfy([0, 1, 6]), true); @@ -155,13 +150,13 @@ describe('THolder', () => { ); assert.deepStrictEqual(tholder.thold, [ [ - math.fraction(1, 2), - math.fraction(1, 2), - math.fraction(1, 4), - math.fraction(1, 4), - math.fraction(1, 4), + fraction(1, 2), + fraction(1, 2), + fraction(1, 4), + fraction(1, 4), + fraction(1, 4), ], - [math.fraction(1, 1), math.fraction(1, 1)], + [fraction(1, 1), fraction(1, 1)], ]); assert.equal(tholder.satisfy([1, 2, 3, 5]), true); assert.equal(tholder.satisfy([0, 1, 6]), true); From f1f1a8993b1dbba268db95577378e3330cd094eb Mon Sep 17 00:00:00 2001 From: Kevin Griffin Date: Thu, 11 Apr 2024 12:48:54 -0400 Subject: [PATCH 13/18] Update main.yml (#247) maybe fix upload issue --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aa3026cd..376a02e6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,7 +47,7 @@ jobs: run: npm run build - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 72826cfe5e3c5b0c872b59b5d20c266d914d87ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lenksj=C3=B6?= <5889538+lenkan@users.noreply.github.com> Date: Thu, 11 Apr 2024 18:50:29 +0200 Subject: [PATCH 14/18] add integration test matrix for keria version (#245) --- .github/workflows/main.yml | 15 ++++++++++++--- README.md | 24 ++++++++++++++++-------- docker-compose.yaml | 2 +- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 376a02e6..662cb3e6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,20 +52,29 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} test: - name: Run integration test - runs-on: ubuntu-latest + name: Run integration test using keria:${{ matrix.keria-version }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: ['ubuntu-latest'] + keria-version: ['latest', '0.1.2', '0.1.3'] + node-version: ['20'] + env: + KERIA_IMAGE_TAG: ${{ matrix.keria-version }} steps: - name: Checkout repo uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '18' + node-version: ${{ matrix.node-version }} cache: 'npm' - name: install deps run: npm ci - name: Build run: npm run build + - name: Print docker compose config + run: docker compose config - name: Start dependencies run: docker compose up deps --pull always - name: Run integration test diff --git a/README.md b/README.md index 22635f96..0ca1bf72 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,22 @@ signify-ts-deps-1 | Dependencies running signify-ts-deps-1 exited with code 0 ``` +It is possible to change the keria image by using environment variables. For example, to use weboftrust/keria:0.1.3, do: + +```bash +export KERIA_IMAGE_TAG=0.1.3 +docker compose pull +docker compose up deps +``` + +To use another repository, you can do: + +```bash +export KERIA_IMAGE=gleif/keria +docker compose pull +docker compose up deps +``` + **Important!** The integration tests runs on the build output in `dist/` directory. Make sure to run build before running the integration tests. ```bash @@ -101,14 +117,6 @@ TEST_ENVIRONMENT=local npx jest examples/integration-scripts/credentials.test.ts This changes the discovery urls to use `localhost` instead of the hostnames inside the docker network. -### Old integration scripts - -To run any of the old integration scripts that has not yet been converted to an integration test. Use `ts-node-esm` - -```bash -npx ts-node-esm examples/integration-scripts/challenge.ts -``` - # Diagrams Account Creation Workflow diff --git a/docker-compose.yaml b/docker-compose.yaml index 20a7defb..1fb5e6ab 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -26,7 +26,7 @@ services: - 7723:7723 keria: - image: weboftrust/keria:latest + image: ${KERIA_IMAGE:-weboftrust/keria}:${KERIA_IMAGE_TAG:-latest} environment: - KERI_AGENT_CORS=1 - KERI_URL=http://keria:3902 From 39580effcb5420b5cc73766d5e7be929ebe112ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lenksj=C3=B6?= <5889538+lenkan@users.noreply.github.com> Date: Thu, 11 Apr 2024 19:34:36 +0200 Subject: [PATCH 15/18] fix flaky integration test (#244) --- examples/integration-scripts/multisig-vlei-issuance.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/integration-scripts/multisig-vlei-issuance.test.ts b/examples/integration-scripts/multisig-vlei-issuance.test.ts index f6a7b06a..930d95ff 100644 --- a/examples/integration-scripts/multisig-vlei-issuance.test.ts +++ b/examples/integration-scripts/multisig-vlei-issuance.test.ts @@ -1261,9 +1261,10 @@ async function getOrCreateAID( await waitOperation(client, await result.op()); aid = await client.identifiers().get(name); - await client + const op = await client .identifiers() .addEndRole(name, 'agent', client!.agent!.pre); + await waitOperation(client, await op.op()); console.log(name, 'AID:', aid.prefix); } return aid; From 39ca0342a1daffb0bc8be84bd97fbd05c068c8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lenksj=C3=B6?= <5889538+lenkan@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:40:01 +0200 Subject: [PATCH 16/18] Update randomPasscode to return 21 characters (#243) --- src/keri/app/coring.ts | 3 ++- test/app/controller.test.ts | 13 ++++++++++++- test/app/coring.test.ts | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/keri/app/coring.ts b/src/keri/app/coring.ts index a1d6af5b..fab67518 100644 --- a/src/keri/app/coring.ts +++ b/src/keri/app/coring.ts @@ -7,7 +7,8 @@ export function randomPasscode(): string { const raw = libsodium.randombytes_buf(16); const salter = new Salter({ raw: raw }); - return salter.qb64.substring(2); + // https://github.com/WebOfTrust/signify-ts/issues/242 + return salter.qb64.substring(2, 23); } export function randomNonce(): string { diff --git a/test/app/controller.test.ts b/test/app/controller.test.ts index 6550627d..b7cc52c8 100644 --- a/test/app/controller.test.ts +++ b/test/app/controller.test.ts @@ -4,7 +4,7 @@ import libsodium from 'libsodium-wrappers-sumo'; import { openManager } from '../../src/keri/core/manager'; import { Signer } from '../../src/keri/core/signer'; import { MtrDex } from '../../src/keri/core/matter'; -import { Tier } from '../../src'; +import { Tier, randomPasscode } from '../../src'; describe('Controller', () => { it('manage account AID signing and agent verification', async () => { @@ -44,4 +44,15 @@ describe('Controller', () => { 'EIIY2SgE_bqKLl2MlnREUawJ79jTuucvWwh-S6zsSUFo' ); }); + + it('should generate unique controller AIDs per passcode', async () => { + await libsodium.ready; + const passcode1 = randomPasscode(); + const passcode2 = randomPasscode(); + + const controller1 = new Controller(passcode1, Tier.low); + const controller2 = new Controller(passcode2, Tier.low); + + assert.notEqual(controller1.pre, controller2.pre); + }); }); diff --git a/test/app/coring.test.ts b/test/app/coring.test.ts index 15066d31..2ebf6185 100644 --- a/test/app/coring.test.ts +++ b/test/app/coring.test.ts @@ -161,7 +161,7 @@ describe('Coring', () => { it('Random passcode', async () => { await libsodium.ready; const passcode = randomPasscode(); - assert.equal(passcode.length, 22); + assert.equal(passcode.length, 21); }); it('Random nonce', async () => { From 42d3b1438e493f66d2b798705ec9c539e6e78e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lenksj=C3=B6?= <5889538+lenkan@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:41:58 +0200 Subject: [PATCH 17/18] update keria test matrix to only use latest for now (#249) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 662cb3e6..c1744229 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,7 +57,7 @@ jobs: strategy: matrix: os: ['ubuntu-latest'] - keria-version: ['latest', '0.1.2', '0.1.3'] + keria-version: ['latest'] node-version: ['20'] env: KERIA_IMAGE_TAG: ${{ matrix.keria-version }} From 4c0072f62ede449d44136e8db87972ceb90bc760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Lenksj=C3=B6?= <5889538+lenkan@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:47:51 +0200 Subject: [PATCH 18/18] feat: method waiting for long running operation (#236) --- .../test-setup-single-client.test.ts | 2 +- .../integration-scripts/utils/test-util.ts | 91 ++----- src/keri/app/coring.ts | 58 ++++- test/app/coring.test.ts | 233 +++++++++++++++--- 4 files changed, 284 insertions(+), 100 deletions(-) diff --git a/examples/integration-scripts/test-setup-single-client.test.ts b/examples/integration-scripts/test-setup-single-client.test.ts index ecbf4e81..cf2f941b 100644 --- a/examples/integration-scripts/test-setup-single-client.test.ts +++ b/examples/integration-scripts/test-setup-single-client.test.ts @@ -1,7 +1,7 @@ import { SignifyClient } from 'signify-ts'; import { getOrCreateClients, getOrCreateIdentifier } from './utils/test-setup'; import { resolveEnvironment } from './utils/resolve-env'; -import { assertOperations, waitOperation } from './utils/test-util'; +import { assertOperations } from './utils/test-util'; let client: SignifyClient; let name1_id: string, name1_oobi: string; diff --git a/examples/integration-scripts/utils/test-util.ts b/examples/integration-scripts/utils/test-util.ts index 4a08262b..95feb44f 100644 --- a/examples/integration-scripts/utils/test-util.ts +++ b/examples/integration-scripts/utils/test-util.ts @@ -15,8 +15,8 @@ export function sleep(ms: number): Promise { export async function assertOperations( ...clients: SignifyClient[] ): Promise { - for (let client of clients) { - let operations = await client.operations().list(); + for (const client of clients) { + const operations = await client.operations().list(); expect(operations).toHaveLength(0); } } @@ -30,9 +30,9 @@ export async function assertOperations( export async function assertNotifications( ...clients: SignifyClient[] ): Promise { - for (let client of clients) { - let res = await client.notifications().list(); - let notes = res.notes.filter((i: any) => i.r === false); + for (const client of clients) { + const res = await client.notifications().list(); + const notes = res.notes.filter((i: { r: boolean }) => i.r === false); expect(notes).toHaveLength(0); } } @@ -46,9 +46,9 @@ export async function warnNotifications( ...clients: SignifyClient[] ): Promise { let count = 0; - for (let client of clients) { - let res = await client.notifications().list(); - let notes = res.notes.filter((i: any) => i.r === false); + for (const client of clients) { + const res = await client.notifications().list(); + const notes = res.notes.filter((i: { r: boolean }) => i.r === false); if (notes.length > 0) { count += notes.length; console.warn('notifications', notes); @@ -57,30 +57,6 @@ export async function warnNotifications( expect(count).toBeGreaterThan(0); // replace warnNotifications with assertNotifications } -/** - * Get status of operation. - * If parameter recurse is set then also checks status of dependent operations. - */ -async function getOperation( - client: SignifyClient, - name: string, - recurse?: boolean -): Promise> { - const result = await client.operations().get(name); - if (recurse === true) { - let i: Operation | undefined = result; - while (result.done && i?.metadata?.depends !== undefined) { - let depends: Operation = await client - .operations() - .get(i.metadata.depends.name); - result.done = result.done && depends.done; - i.metadata.depends = depends; - i = depends.metadata?.depends; - } - } - return result; -} - /** * Poll for operation to become completed. * Removes completed operation @@ -88,42 +64,29 @@ async function getOperation( export async function waitOperation( client: SignifyClient, op: Operation | string, - options: RetryOptions = {} + signal?: AbortSignal ): Promise> { - const ctrl = new AbortController(); - options.signal?.addEventListener('abort', (e: Event) => { - const s = e.target as AbortSignal; - ctrl.abort(s.reason); - }); - let name: string; if (typeof op === 'string') { - name = op; - } else if (typeof op === 'object' && 'name' in op) { - name = op.name; - } else { - throw new Error(); + op = await client.operations().get(op); } - const result: Operation = await retry(async () => { - let t: Operation; - try { - t = await getOperation(client, name, true); - } catch (e) { - ctrl.abort(e); - throw e; - } - if (t.done !== true) { - throw new Error(`Operation ${name} not done`); - } - console.log('DONE', name); - return t; - }, options); - let i: Operation | undefined = result; - while (i !== undefined) { - // console.log('DELETE', i.name); - await client.operations().delete(i.name); - i = i.metadata?.depends; + + op = await client + .operations() + .wait(op, { signal: signal ?? AbortSignal.timeout(30000) }); + await deleteOperations(client, op); + + return op; +} + +async function deleteOperations( + client: SignifyClient, + op: Operation +) { + if (op.metadata?.depends) { + await deleteOperations(client, op.metadata.depends); } - return result; + + await client.operations().delete(op.name); } export async function resolveOobi( diff --git a/src/keri/app/coring.ts b/src/keri/app/coring.ts index fab67518..fd5a1161 100644 --- a/src/keri/app/coring.ts +++ b/src/keri/app/coring.ts @@ -72,18 +72,27 @@ export interface Operation { response?: T; } +export interface OperationsDeps { + fetch( + pathname: string, + method: string, + body: unknown, + headers?: Headers + ): Promise; +} + /** * Operations * @remarks * Operations represent the status and result of long running tasks performed by KERIA agent */ export class Operations { - public client: SignifyClient; + public client: OperationsDeps; /** * Operations * @param {SignifyClient} client */ - constructor(client: SignifyClient) { + constructor(client: OperationsDeps) { this.client = client; } @@ -128,6 +137,51 @@ export class Operations { const method = 'DELETE'; await this.client.fetch(path, method, data); } + + /** + * Poll for operation to become completed. + */ + async wait( + op: Operation, + options: { + signal?: AbortSignal; + minSleep?: number; + maxSleep?: number; + increaseFactor?: number; + } = {} + ): Promise> { + const minSleep = options.minSleep ?? 10; + const maxSleep = options.maxSleep ?? 10000; + const increaseFactor = options.increaseFactor ?? 50; + + if (op.metadata?.depends?.done === false) { + await this.wait(op.metadata.depends, options); + } + + if (op.done === true) { + return op; + } + + let retries = 0; + + // eslint-disable-next-line no-constant-condition + while (true) { + op = await this.get(op.name); + + const delay = Math.max( + minSleep, + Math.min(maxSleep, 2 ** retries * increaseFactor) + ); + retries++; + + if (op.done === true) { + return op; + } + + await new Promise((resolve) => setTimeout(resolve, delay)); + options.signal?.throwIfAborted(); + } + } } /** diff --git a/test/app/coring.test.ts b/test/app/coring.test.ts index 2ebf6185..f25c8be0 100644 --- a/test/app/coring.test.ts +++ b/test/app/coring.test.ts @@ -1,11 +1,17 @@ import { strict as assert } from 'assert'; import libsodium from 'libsodium-wrappers-sumo'; -import { randomPasscode, randomNonce } from '../../src/keri/app/coring'; +import { + randomPasscode, + randomNonce, + Operations, + OperationsDeps, +} from '../../src/keri/app/coring'; import { SignifyClient } from '../../src/keri/app/clienting'; import { Authenticater } from '../../src/keri/core/authing'; import { Salter, Tier } from '../../src/keri/core/salter'; import fetchMock from 'jest-fetch-mock'; import 'whatwg-fetch'; +import { randomUUID } from 'crypto'; fetchMock.enableMocks(); @@ -202,38 +208,6 @@ describe('Coring', () => { assert.deepEqual(lastBody.oobialias, 'witness'); }); - it('Operations', async () => { - await libsodium.ready; - const bran = '0123456789abcdefghijk'; - - const client = new SignifyClient(url, bran, Tier.low, boot_url); - - await client.boot(); - await client.connect(); - - const ops = client.operations(); - - await ops.get('operationName'); - let lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/operations/operationName'); - assert.equal(lastCall[1]!.method, 'GET'); - - await ops.list(); - lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/operations?'); - assert.equal(lastCall[1]!.method, 'GET'); - - await ops.list('witness'); - lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/operations?type=witness'); - assert.equal(lastCall[1]!.method, 'GET'); - - await ops.delete('operationName'); - lastCall = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]!; - assert.equal(lastCall[0]!, url + '/operations/operationName'); - assert.equal(lastCall[1]!.method, 'DELETE'); - }); - it('Events and states', async () => { await libsodium.ready; const bran = '0123456789abcdefghijk'; @@ -294,3 +268,196 @@ describe('Coring', () => { ); }); }); + +describe('Operations', () => { + class MockClient implements OperationsDeps { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fetch = jest.fn, [string, string, any]>(); + + constructor() {} + + operations() { + return new Operations(this); + } + + getLastMockRequest() { + const [pathname, method, body] = this.fetch.mock.lastCall ?? []; + + return { + path: pathname, + method: method, + body: body, + }; + } + } + + let client: MockClient; + beforeEach(async () => { + await libsodium.ready; + client = new MockClient(); + }); + + it('Can get operation by name', async () => { + await libsodium.ready; + + client.fetch.mockResolvedValue( + new Response(JSON.stringify({ name: randomUUID() }), { + status: 200, + }) + ); + await client.operations().get('operationName'); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/operations/operationName'); + assert.equal(lastCall.method, 'GET'); + }); + + it('Can list operations', async () => { + client.fetch.mockResolvedValue( + new Response(JSON.stringify([]), { + status: 200, + }) + ); + await client.operations().list(); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/operations?'); + assert.equal(lastCall.method, 'GET'); + }); + + it('Can list operations by type', async () => { + client.fetch.mockResolvedValue( + new Response(JSON.stringify([]), { + status: 200, + }) + ); + await client.operations().list('witness'); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/operations?type=witness'); + assert.equal(lastCall.method, 'GET'); + }); + + it('Can delete operation by name', async () => { + client.fetch.mockResolvedValue( + new Response(JSON.stringify({}), { + status: 200, + }) + ); + await client.operations().delete('operationName'); + const lastCall = client.getLastMockRequest(); + assert.equal(lastCall.path, '/operations/operationName'); + assert.equal(lastCall.method, 'DELETE'); + }); + + describe('wait', () => { + it('does not wait for operation that is already "done"', async () => { + const name = randomUUID(); + client.fetch.mockResolvedValue( + new Response(JSON.stringify({ name }), { + status: 200, + }) + ); + + const op = { name, done: true }; + const result = await client.operations().wait(op); + assert.equal(client.fetch.mock.calls.length, 0); + assert.equal(op, result); + }); + + it('returns when operation is done after first call', async () => { + const name = randomUUID(); + client.fetch.mockResolvedValue( + new Response(JSON.stringify({ name, done: true }), { + status: 200, + }) + ); + + const op = { name, done: false }; + await client.operations().wait(op); + assert.equal(client.fetch.mock.calls.length, 1); + }); + + it('returns when operation is done after second call', async () => { + const name = randomUUID(); + client.fetch.mockResolvedValueOnce( + new Response(JSON.stringify({ name, done: false }), { + status: 200, + }) + ); + + client.fetch.mockResolvedValueOnce( + new Response(JSON.stringify({ name, done: true }), { + status: 200, + }) + ); + + const op = { name, done: false }; + await client.operations().wait(op, { maxSleep: 10 }); + assert.equal(client.fetch.mock.calls.length, 2); + }); + + it('throw if aborted', async () => { + const name = randomUUID(); + client.fetch.mockImplementation( + async () => + new Response(JSON.stringify({ name, done: false }), { + status: 200, + }) + ); + + const op = { name, done: false }; + + const controller = new AbortController(); + const promise = client + .operations() + .wait(op, { signal: controller.signal }) + .catch((e) => e); + + const abortError = new Error('Aborted'); + controller.abort(abortError); + + const error = await promise; + + assert.equal(error, abortError); + }); + + it('returns when child operation is also done', async () => { + const name = randomUUID(); + const nestedName = randomUUID(); + const depends = { name: nestedName, done: false }; + const op = { name, done: false, depends }; + + client.fetch.mockResolvedValueOnce( + new Response(JSON.stringify({ ...op, done: false }), { + status: 200, + }) + ); + + client.fetch.mockResolvedValueOnce( + new Response( + JSON.stringify({ + ...op, + depends: { ...depends, done: true }, + }), + { + status: 200, + } + ) + ); + + client.fetch.mockResolvedValueOnce( + new Response( + JSON.stringify({ + ...op, + done: true, + depends: { ...depends, done: true }, + }), + { + status: 200, + } + ) + ); + + await client.operations().wait(op, { maxSleep: 10 }); + assert.equal(client.fetch.mock.calls.length, 3); + }); + }); +});