From 97346e1422b300776f1dc18774166bb4abd980be Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Wed, 19 Apr 2023 16:01:50 -0400 Subject: [PATCH 01/19] Commit to resolve merge conflicts Signed-off-by: Frank Hinek --- examples/simple-agent/.gitignore | 8 +- examples/simple-agent/etc/did.json | 6 +- examples/simple-agent/src/index.js | 14 +- examples/simple-agent/src/utils.js | 21 +- .../desktop-agent-original.html | 937 +++++++++++++++ examples/test-dashboard/desktop-agent.html | 1061 +++++++++-------- examples/test-dashboard/simple-agent.html | 242 ++-- karma.conf.cjs | 2 +- package-lock.json | 452 ++++++- package.json | 7 +- src/Web5.js | 61 +- src/did/connect/connect.js | 46 +- src/did/connect/utils.js | 22 +- src/did/connect/ws-client.js | 2 - src/did/crypto/x25519-xsalsa20-poly1305.js | 4 +- src/did/manager.js | 22 + src/did/methods/ion.js | 8 +- src/did/methods/key.js | 2 +- src/did/{didUtils.js => utils.js} | 12 +- src/did/{Web5DID.js => web5-did.js} | 63 +- src/dwn/dwn-utils.js | 7 + src/dwn/interface/Records.js | 37 - .../Interface.js => interfaces/interface.js} | 10 +- .../permissions.js} | 2 +- .../Protocols.js => interfaces/protocols.js} | 2 +- src/dwn/interfaces/records.js | 103 ++ src/dwn/models/record.js | 244 ++++ src/dwn/{Web5DWN.js => web5-dwn.js} | 18 +- src/main.js | 2 +- src/storage/Storage.js | 2 +- .../{LocalStorage.js => local-storage.js} | 4 +- .../{MemoryStorage.js => memory-storage.js} | 6 +- src/storage/storage.js | 21 + .../{AppTransport.js => app-transport.js} | 8 +- .../{HTTPTransport.js => http-transport.js} | 35 +- src/transport/transport.js | 19 + src/types.js | 230 ++++ src/utils.js | 28 +- src/web5.js | 137 +++ .../{didDocuments.js => did-documents.js} | 0 tests/did/methods/ion.spec.js | 39 +- tests/did/methods/key.spec.js | 45 +- tests/did/{didUtils.spec.js => utils.spec.js} | 6 +- .../did/{Web5DID.spec.js => web5-did.spec.js} | 31 +- ...Storage.spec.js => memory-storage.spec.js} | 8 +- 45 files changed, 3133 insertions(+), 903 deletions(-) create mode 100644 examples/test-dashboard/desktop-agent-original.html create mode 100644 src/did/manager.js rename src/did/{didUtils.js => utils.js} (97%) rename src/did/{Web5DID.js => web5-did.js} (80%) create mode 100644 src/dwn/dwn-utils.js delete mode 100644 src/dwn/interface/Records.js rename src/dwn/{interface/Interface.js => interfaces/interface.js} (87%) rename src/dwn/{interface/Permissions.js => interfaces/permissions.js} (90%) rename src/dwn/{interface/Protocols.js => interfaces/protocols.js} (87%) create mode 100644 src/dwn/interfaces/records.js create mode 100644 src/dwn/models/record.js rename src/dwn/{Web5DWN.js => web5-dwn.js} (67%) rename src/storage/{LocalStorage.js => local-storage.js} (83%) rename src/storage/{MemoryStorage.js => memory-storage.js} (89%) create mode 100644 src/storage/storage.js rename src/transport/{AppTransport.js => app-transport.js} (80%) rename src/transport/{HTTPTransport.js => http-transport.js} (59%) create mode 100644 src/transport/transport.js create mode 100644 src/types.js create mode 100644 src/web5.js rename tests/data/{didDocuments.js => did-documents.js} (100%) rename tests/did/{didUtils.spec.js => utils.spec.js} (98%) rename tests/did/{Web5DID.spec.js => web5-did.spec.js} (89%) rename tests/storage/{MemoryStorage.spec.js => memory-storage.spec.js} (92%) diff --git a/examples/simple-agent/.gitignore b/examples/simple-agent/.gitignore index 0467ea0eb..cf58023e7 100644 --- a/examples/simple-agent/.gitignore +++ b/examples/simple-agent/.gitignore @@ -1,5 +1,3 @@ -# Agent Specific - -BLOCKSTORE/ -DATASTORE/ -INDEX/ \ No newline at end of file +DATASTORE-* +INDEX-* +MESSAGESTORE-* \ No newline at end of file diff --git a/examples/simple-agent/etc/did.json b/examples/simple-agent/etc/did.json index b3ddb677d..d18a28ca7 100644 --- a/examples/simple-agent/etc/did.json +++ b/examples/simple-agent/etc/did.json @@ -7,7 +7,7 @@ "content": { "publicKeys": [ { - "id": "key-1", + "id": "dwn", "type": "JsonWebKey2020", "purposes": [ "authentication" @@ -68,9 +68,9 @@ ], "keys": [ { - "id": "key-1", + "id": "dwn", "type": "JsonWebKey2020", - "keypair": { + "keyPair": { "publicJwk": { "kty": "EC", "crv": "secp256k1", diff --git a/examples/simple-agent/src/index.js b/examples/simple-agent/src/index.js index 33a72f75f..c148bb011 100644 --- a/examples/simple-agent/src/index.js +++ b/examples/simple-agent/src/index.js @@ -20,10 +20,16 @@ router.post('/dwn', async (ctx, _next) => { try { const response = await receiveHttp(ctx); - // Normalize DWN MessageReply and HTTP Reponse - ctx.status = response?.status?.code ?? response?.status; - ctx.statusText = response?.status?.detail ?? response?.statusText; - ctx.body = 'entries' in response ? { entries: response.entries } : response.body; + console.log('SIMPLE AGENT receiveHTTP response:', response); + + // // All DWN MessageReply responses contain a `status` object. + // // DWN RecordsQuery responses contain an `entries` array of query results. + // // DWN RecordsRead responses contain a data property which is a Readable stream. + // const { message, ...retainedResponse } = response; + + // ctx.body = retainedResponse; + // ctx.status = retainedResponse?.status?.code; + // ctx.statusText = retainedResponse?.status?.detail; } catch(err) { console.error(err); diff --git a/examples/simple-agent/src/utils.js b/examples/simple-agent/src/utils.js index 27784bc2f..2384f2fb2 100644 --- a/examples/simple-agent/src/utils.js +++ b/examples/simple-agent/src/utils.js @@ -1,10 +1,22 @@ import getRawBody from 'raw-body'; +import { DataStoreLevel, Dwn, MessageStoreLevel } from '@tbd54566975/dwn-sdk-js'; import { Web5 } from '@tbd54566975/web5'; import fs from 'node:fs'; import mkdirp from 'mkdirp'; import { createRequire } from 'node:module'; -const web5 = new Web5(); +// Use custom names for the block, message, and data stores to make it possible to launch multiple simple agents +// in the same directory. If you don't do this, you will get LevelDB lock errors. +const port = await getPort(process.argv); +const dataStore = new DataStoreLevel({ blockstoreLocation: `DATASTORE-${port}` }); +const messageStore = new MessageStoreLevel({ + blockstoreLocation : `MESSAGESTORE-${port}`, + indexLocation : `INDEX-${port}`, +}); + +const dwnNode = await Dwn.create({ messageStore, dataStore }); + +const web5 = new Web5({ dwn: { node: dwnNode }}); const etcPath = './etc'; const didStoragePath = `${etcPath}/did.json`; @@ -42,11 +54,10 @@ async function loadConfig() { didState = await initOperatorDid(); fs.writeFileSync(didStoragePath, JSON.stringify(didState, null, 2)); } - web5.did.register({ + web5.did.manager.set(didState.id, { connected: true, - did: didState.id, endpoint: 'app://dwn', - keys: didState.keys[0].keypair, + keys: didState.keys['#dwn'].keyPair, }); } @@ -99,8 +110,6 @@ async function receiveHttp(ctx) { const data = await getRawBody(ctx.req); - console.log('TRACE - receiveHttp() - message:', message); - return await web5.send(target, { author, data, diff --git a/examples/test-dashboard/desktop-agent-original.html b/examples/test-dashboard/desktop-agent-original.html new file mode 100644 index 000000000..df1527f12 --- /dev/null +++ b/examples/test-dashboard/desktop-agent-original.html @@ -0,0 +1,937 @@ + + + + + + + Agent Test Dashboard + + + + + + + +
+
+ DID Connect +
+ + +
+ + +
+
+ +
+

Write Records

+ +
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+
+ +
+ +

Query Records

+ +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+ +

Read Records

+ +
+
+ +
+
+ +
+
+ + + +
+
+ +
+ +

Delete Records

+ +
+
+ + + +
+
+ +
+ +

Protocols

+ +
+
+
+ + + +
+
+ +
+ + + + + + +
+
+ +
+ +

Custom Data Formats

+ +
+
+ + + + + + + + + +
+
+ +
+
+ + + + + + + +
+
+ +
+ +

New Tests

+ +
+
+ Write data authored by Alice's DID to Alice's DWN WITH local key chain + +
+
+ +
+
+ Write data authored by Alice's DID to Alice's DWN withOUT local key chain + +
+
+ +
+
+ Write data authored by Alice's DID to Bob's DWN WITH local key chain + +
+
+ +
+
+ Write data authored by Alice's DID to Bob's DWN withOUT local key chain + +
+
+ +
+
+ Query data authored by Alice's DID to Bob's DWN withOUT local key chain + +
+
+ + +
+ + + + + \ No newline at end of file diff --git a/examples/test-dashboard/desktop-agent.html b/examples/test-dashboard/desktop-agent.html index 3b2830875..f09f5ab5b 100644 --- a/examples/test-dashboard/desktop-agent.html +++ b/examples/test-dashboard/desktop-agent.html @@ -23,6 +23,7 @@ font-family: 'IBM Plex Mono'; padding: 0; margin: 0; + max-width: 72rem; } input, button { @@ -31,50 +32,35 @@ main { color: rgb(250, 250, 250); - padding: 0rem 1rem; } - .box { - max-width: 52rem; - display: flex; - flex-direction: column; - } - - .box>.row { - display: flex; - align-items: start; - padding-top: 0.5em; - } - - .box>.row>label { - padding-right: 0.5em; - padding-left: 0.5em; + fieldset { + border-width: 2px; + border-style: solid; + border-radius: 5px; + padding: 1.25rem; + padding-block-start: 0.5em; + margin: 0rem 1rem 2rem 1rem; } - - .box>.row>button { - margin-left: auto; + + fieldset.yellow { + border-color: var(--color-yellow); } - .box>.row>input[type=text] { - width: 8em; + fieldset.yellow>legend { + color: var(--color-yellow); } - - .box>.row>textarea { - width: 250px; - height: 125px; + + fieldset.blue { + border-color: var(--color-blue); } - fieldset { - border: 2px solid var(--color-yellow); - border-radius: 5px; - padding: 20px; - margin: 0rem 1rem; + fieldset.blue>legend { + color: var(--color-blue); } legend { - color: var(--color-yellow); font-size: 1.5rem; - padding: 0 10px; } fieldset .buttons { @@ -90,12 +76,13 @@ color: white; cursor: pointer; font-size: 14px; + min-width:fit-content; + white-space: nowrap; padding: 0.5em 20px; text-align: center; } button:hover { - /* background-color: #45a049; */ filter: brightness(95%); } @@ -104,6 +91,20 @@ font-size: 1rem; margin-top: 1rem; text-align: center; + + /* for smooth appearance transition */ + opacity: 0; + max-height: 0; + overflow: hidden; + margin: 0; + padding: 0; + transition: opacity 0.4s ease, max-height 0.4s ease, margin 0.4s ease, padding 0.4s ease; + } + + #container_connect_status.visible { + opacity: 1; + max-height: 1000px; + margin-top: 1rem; } #container_connect_status.success { @@ -117,6 +118,20 @@ #container_security_code { margin-top: 1rem; text-align: center; + + /* for smooth appearance transition */ + opacity: 0; + max-height: 0; + overflow: hidden; + margin: 0; + padding: 0; + transition: opacity 0.4s ease, max-height 0.4s ease, margin 0.4s ease, padding 0.4s ease; + } + + #container_security_code.visible { + opacity: 1; + max-height: 1000px; + margin-top: 1rem; } #container_security_code .label { @@ -143,19 +158,75 @@ background-color: var(--color-blue-10); line-height: 2.5rem; /* Vertically center the text */ } + + main #output { + padding: 0 1rem; + } + + main #output .row { + background-color: rgb(51, 51, 51); + border: none; + border-radius: 0.5rem; + font-size: 0.75rem; + margin: 1rem 0rem; + padding: 1rem; + white-space: pre-wrap; + } + + main #output .error { + color: var(--color-red); + } + + main .button-container { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-auto-rows: 2rem; + column-gap: 1rem; + row-gap: 1rem; + width: 100%; + min-width: max-content; + /* max-width: 37.5rem; */ + margin: 0 auto; + box-sizing: border-box; + } + + @media (max-width: 27em) { + main .button-container { + grid-template-columns: repeat(2, 1fr); + } + } + + main .output-status { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 1rem; + } + + main .output-status .btn { + background-color: var(--color-red); + filter: brightness(70%); + } + + main .output-status .btn:hover { + background-color: var(--color-red); + filter: brightness(95%); + }
-
+
DID Connect
- -
-

Write Records

- -
-
- - - - - - -
- -
- - - - - - -
- -
- - - - - - -
-
- -
- -

Query Records

- -
-
- - - -
-
- -
-
- - - -
-
- -
-
- - - -
-
- -
- -

Read Records

- -
-
- -
-
- -
-
- - - -
-
- -
- -

Delete Records

- -
-
- - - -
-
- -
- -

Protocols

- -
-
-
- - - -
-
- -
- - - - - - +
+ Records Functions +
+ + + + + + + + + + + + + + + + + + +
-
- -
- -

Custom Data Formats

- -
-
- - - - - - - - - -
-
- -
-
- - - - - - - -
-
- -
- -

New Tests

- -
-
- Write data authored by Alice's DID to Alice's DWN WITH local key chain - -
-
- -
-
- Write data authored by Alice's DID to Alice's DWN withOUT local key chain - -
-
- -
-
- Write data authored by Alice's DID to Bob's DWN WITH local key chain - -
-
- -
-
- Write data authored by Alice's DID to Bob's DWN withOUT local key chain - + + +
+ Record Instance +
+ + + +
-
- -
-
- Query data authored by Alice's DID to Bob's DWN withOUT local key chain - -
+ + +
+
+
+
- +
diff --git a/examples/test-dashboard/simple-agent.html b/examples/test-dashboard/simple-agent.html index 01f33e152..cff04b2c0 100644 --- a/examples/test-dashboard/simple-agent.html +++ b/examples/test-dashboard/simple-agent.html @@ -112,6 +112,7 @@

Read Records

diff --git a/karma.conf.cjs b/karma.conf.cjs index 210dd3ff3..be5a485fa 100644 --- a/karma.conf.cjs +++ b/karma.conf.cjs @@ -29,7 +29,7 @@ module.exports = function (config) { // list of files / patterns to load in the browser files: [ - { pattern: 'tests/**/*.spec.js', watched: false } + { pattern: 'tests/**/*.spec.js', watched: false }, ], // preprocess matching files before serving them to the browser diff --git a/package-lock.json b/package-lock.json index 470b3632e..a5e021d25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,24 @@ { "name": "@tbd54566975/web5", - "version": "0.5.0", + "version": "0.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@tbd54566975/web5", - "version": "0.5.0", + "version": "0.6.0", "license": "Apache-2.0", "dependencies": { "@decentralized-identity/ion-tools": "1.0.6", - "@tbd54566975/dwn-sdk-js": "0.0.26", + "@tbd54566975/dwn-sdk-js": "0.0.30-unstable-2023-04-15-149c713", "cross-fetch": "3.1.5", "ed2curve": "0.3.0", + "readable-web-to-node-stream": "3.0.2", "tweetnacl": "1.0.3" }, "devDependencies": { "chai": "4.3.7", - "chai-as-promised": "^7.1.1", + "chai-as-promised": "7.1.1", "esbuild": "0.16.17", "eslint": "8.36.0", "karma": "6.4.1", @@ -852,6 +853,17 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", @@ -917,23 +929,27 @@ "integrity": "sha512-aWItSZvJj4+GI6FWkjZR13xPNPctq2RRakzo+O6vN7bC2yjwdg5EFpgaSAUn95b7BGSgcflvzVDPoKmJv24IOg==" }, "node_modules/@tbd54566975/dwn-sdk-js": { - "version": "0.0.26", - "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.26.tgz", - "integrity": "sha512-Bj+cesS5ePQ1a8k+x5DFDQFwL8gNidOicW+/+VC/KjMH2QLikCuWYdcW9A3toxPJqtTQLlfXDBp5vLeO94C7/Q==", + "version": "0.0.30-unstable-2023-04-15-149c713", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.30-unstable-2023-04-15-149c713.tgz", + "integrity": "sha512-3xSbZ4DMvQUqCActdwuRRDfFVDjMHsX3tFxEn4u5O6SOmTdZ44AZQwxS8F0hINS5onnGjgfZNXc/NZ6FBmunDA==", "dependencies": { "@ipld/dag-cbor": "9.0.0", "@js-temporal/polyfill": "0.4.3", "@noble/ed25519": "1.7.1", "@noble/secp256k1": "1.7.1", + "@scure/base": "1.1.1", "@swc/helpers": "0.3.8", + "@types/eccrypto": "1.1.3", "@types/ms": "0.7.31", "@types/node": "^18.13.0", "@types/readable-stream": "2.3.15", + "@types/secp256k1": "4.0.3", "abstract-level": "1.0.3", "ajv": "8.11.0", "blockstore-core": "3.0.0", "cross-fetch": "3.1.5", "date-fns": "2.28.0", + "eccrypto": "1.1.6", "flat": "^5.0.2", "interface-blockstore": "4.0.1", "ipfs-unixfs": "6.0.9", @@ -946,6 +962,8 @@ "multiformats": "11.0.2", "randombytes": "2.1.0", "readable-stream": "4.3.0", + "secp256k1": "5.0.0", + "ulid": "2.3.0", "uuid": "8.3.2", "varint": "6.0.0" }, @@ -1012,6 +1030,15 @@ "@types/node": "*" } }, + "node_modules/@types/eccrypto": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/eccrypto/-/eccrypto-1.1.3.tgz", + "integrity": "sha512-3O0qER6JMYReqVbcQTGmXeMHdw3O+rVps63tlo5g5zoB3altJS8yzSvboSivwVWeYO9o5jSATu7P0UIqYZPgow==", + "dependencies": { + "@types/expect": "^1.20.4", + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "8.21.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz", @@ -1041,6 +1068,11 @@ "dev": true, "peer": true }, + "node_modules/@types/expect": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", + "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==" + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -1072,6 +1104,14 @@ "safe-buffer": "~5.1.1" } }, + "node_modules/@types/secp256k1": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.3.tgz", + "integrity": "sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -1483,6 +1523,24 @@ "node": ">=8" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -1602,8 +1660,7 @@ "node_modules/brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, "node_modules/browser-level": { "version": "1.0.1", @@ -1635,7 +1692,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, + "devOptional": true, "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -1801,7 +1858,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true + "devOptional": true }, "node_modules/builtin-status-codes": { "version": "3.0.0", @@ -1999,7 +2056,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, + "devOptional": true, "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -2159,7 +2216,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, + "devOptional": true, "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -2172,7 +2229,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, + "devOptional": true, "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -2432,6 +2489,72 @@ "url": "https://bevry.me/fund" } }, + "node_modules/drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==", + "optional": true, + "dependencies": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/eccrypto": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/eccrypto/-/eccrypto-1.1.6.tgz", + "integrity": "sha512-d78ivVEzu7Tn0ZphUUaL43+jVPKTMPFGtmgtz1D0LrFn7cY3K8CdrvibuLz2AAkHBLKZtR8DMbB2ukRYFk987A==", + "hasInstallScript": true, + "dependencies": { + "acorn": "7.1.1", + "elliptic": "6.5.4", + "es6-promise": "4.2.8", + "nan": "2.14.0" + }, + "optionalDependencies": { + "secp256k1": "3.7.1" + } + }, + "node_modules/eccrypto/node_modules/acorn": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eccrypto/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "optional": true + }, + "node_modules/eccrypto/node_modules/secp256k1": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.7.1.tgz", + "integrity": "sha512-1cf8sbnRreXrQFdH6qsg2H71Xw91fCCS9Yp021GnUNJzWJS/py96fS4lHbnTnouLp08Xj6jBoBB6V78Tdbdu5g==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.4.1", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/ed2curve": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/ed2curve/-/ed2curve-0.3.0.tgz", @@ -2457,7 +2580,6 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -2471,8 +2593,7 @@ "node_modules/elliptic/node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -2580,6 +2701,11 @@ "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==", "dev": true }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "node_modules/esbuild": { "version": "0.16.17", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", @@ -3007,7 +3133,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, + "devOptional": true, "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -3057,6 +3183,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3413,7 +3545,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, + "devOptional": true, "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -3427,7 +3559,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "devOptional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -3441,7 +3573,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -3466,7 +3598,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -3485,7 +3616,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -4583,7 +4713,7 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, + "devOptional": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -4661,14 +4791,12 @@ "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" }, "node_modules/minimatch": { "version": "3.1.2", @@ -5002,6 +5130,11 @@ "node": ">=8.0.0" } }, + "node_modules/nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, "node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -5063,6 +5196,11 @@ "type-detect": "4.0.8" } }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -5778,6 +5916,34 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5919,7 +6085,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, + "devOptional": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -6034,6 +6200,20 @@ "dev": true, "peer": true }, + "node_modules/secp256k1": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.0.tgz", + "integrity": "sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA==", + "hasInstallScript": true, + "dependencies": { + "elliptic": "^6.5.4", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -6059,7 +6239,7 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, + "devOptional": true, "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -6772,6 +6952,14 @@ "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" }, + "node_modules/ulid": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==", + "bin": { + "ulid": "bin/cli.js" + } + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -7745,6 +7933,11 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" + }, "@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", @@ -7814,23 +8007,27 @@ "integrity": "sha512-aWItSZvJj4+GI6FWkjZR13xPNPctq2RRakzo+O6vN7bC2yjwdg5EFpgaSAUn95b7BGSgcflvzVDPoKmJv24IOg==" }, "@tbd54566975/dwn-sdk-js": { - "version": "0.0.26", - "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.26.tgz", - "integrity": "sha512-Bj+cesS5ePQ1a8k+x5DFDQFwL8gNidOicW+/+VC/KjMH2QLikCuWYdcW9A3toxPJqtTQLlfXDBp5vLeO94C7/Q==", + "version": "0.0.30-unstable-2023-04-15-149c713", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.30-unstable-2023-04-15-149c713.tgz", + "integrity": "sha512-3xSbZ4DMvQUqCActdwuRRDfFVDjMHsX3tFxEn4u5O6SOmTdZ44AZQwxS8F0hINS5onnGjgfZNXc/NZ6FBmunDA==", "requires": { "@ipld/dag-cbor": "9.0.0", "@js-temporal/polyfill": "0.4.3", "@noble/ed25519": "1.7.1", "@noble/secp256k1": "1.7.1", + "@scure/base": "1.1.1", "@swc/helpers": "0.3.8", + "@types/eccrypto": "1.1.3", "@types/ms": "0.7.31", "@types/node": "^18.13.0", "@types/readable-stream": "2.3.15", + "@types/secp256k1": "4.0.3", "abstract-level": "1.0.3", "ajv": "8.11.0", "blockstore-core": "3.0.0", "cross-fetch": "3.1.5", "date-fns": "2.28.0", + "eccrypto": "1.1.6", "flat": "^5.0.2", "interface-blockstore": "4.0.1", "ipfs-unixfs": "6.0.9", @@ -7843,6 +8040,8 @@ "multiformats": "11.0.2", "randombytes": "2.1.0", "readable-stream": "4.3.0", + "secp256k1": "5.0.0", + "ulid": "2.3.0", "uuid": "8.3.2", "varint": "6.0.0" }, @@ -7888,6 +8087,15 @@ "@types/node": "*" } }, + "@types/eccrypto": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/eccrypto/-/eccrypto-1.1.3.tgz", + "integrity": "sha512-3O0qER6JMYReqVbcQTGmXeMHdw3O+rVps63tlo5g5zoB3altJS8yzSvboSivwVWeYO9o5jSATu7P0UIqYZPgow==", + "requires": { + "@types/expect": "^1.20.4", + "@types/node": "*" + } + }, "@types/eslint": { "version": "8.21.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz", @@ -7917,6 +8125,11 @@ "dev": true, "peer": true }, + "@types/expect": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", + "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==" + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -7948,6 +8161,14 @@ "safe-buffer": "~5.1.1" } }, + "@types/secp256k1": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.3.tgz", + "integrity": "sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w==", + "requires": { + "@types/node": "*" + } + }, "@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -8297,6 +8518,24 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -8398,8 +8637,7 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" }, "browser-level": { "version": "1.0.1", @@ -8431,7 +8669,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, + "devOptional": true, "requires": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -8552,7 +8790,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true + "devOptional": true }, "builtin-status-codes": { "version": "3.0.0", @@ -8684,7 +8922,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, + "devOptional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -8826,7 +9064,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, + "devOptional": true, "requires": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -8839,7 +9077,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, + "devOptional": true, "requires": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -9049,6 +9287,58 @@ "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", "dev": true }, + "drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==", + "optional": true, + "requires": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + } + }, + "eccrypto": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/eccrypto/-/eccrypto-1.1.6.tgz", + "integrity": "sha512-d78ivVEzu7Tn0ZphUUaL43+jVPKTMPFGtmgtz1D0LrFn7cY3K8CdrvibuLz2AAkHBLKZtR8DMbB2ukRYFk987A==", + "requires": { + "acorn": "7.1.1", + "elliptic": "6.5.4", + "es6-promise": "4.2.8", + "nan": "2.14.0", + "secp256k1": "3.7.1" + }, + "dependencies": { + "acorn": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==" + }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "optional": true + }, + "secp256k1": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.7.1.tgz", + "integrity": "sha512-1cf8sbnRreXrQFdH6qsg2H71Xw91fCCS9Yp021GnUNJzWJS/py96fS4lHbnTnouLp08Xj6jBoBB6V78Tdbdu5g==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.4.1", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + } + } + } + }, "ed2curve": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/ed2curve/-/ed2curve-0.3.0.tgz", @@ -9074,7 +9364,6 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, "requires": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -9088,8 +9377,7 @@ "bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" } } }, @@ -9181,6 +9469,11 @@ "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==", "dev": true }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "esbuild": { "version": "0.16.17", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", @@ -9502,7 +9795,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, + "devOptional": true, "requires": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -9549,6 +9842,12 @@ "flat-cache": "^3.0.4" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -9806,7 +10105,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, + "devOptional": true, "requires": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -9817,7 +10116,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "devOptional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -9828,7 +10127,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "devOptional": true } } }, @@ -9841,7 +10140,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -9857,7 +10155,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -10669,7 +10966,7 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, + "devOptional": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -10731,14 +11028,12 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" }, "minimatch": { "version": "3.1.2", @@ -10983,6 +11278,11 @@ "resolved": "https://registry.npmjs.org/murmurhash3js-revisited/-/murmurhash3js-revisited-3.0.0.tgz", "integrity": "sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g==" }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, "nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -11037,6 +11337,11 @@ } } }, + "node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -11563,6 +11868,26 @@ "process": "^0.11.10" } }, + "readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "requires": { + "readable-stream": "^3.6.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11663,7 +11988,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, + "devOptional": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -11739,6 +12064,16 @@ } } }, + "secp256k1": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.0.tgz", + "integrity": "sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA==", + "requires": { + "elliptic": "^6.5.4", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.2.0" + } + }, "serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -11764,7 +12099,7 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, + "devOptional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -12292,6 +12627,11 @@ } } }, + "ulid": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==" + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", diff --git a/package.json b/package.json index 37f5caa50..5bf3faf47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tbd54566975/web5", - "version": "0.5.0", + "version": "0.6.0", "description": "SDK for accessing the features and capabilities of Web5", "type": "module", "main": "./dist/cjs/index.cjs", @@ -49,7 +49,7 @@ "license": "Apache-2.0", "devDependencies": { "chai": "4.3.7", - "chai-as-promised": "^7.1.1", + "chai-as-promised": "7.1.1", "esbuild": "0.16.17", "eslint": "8.36.0", "karma": "6.4.1", @@ -69,9 +69,10 @@ }, "dependencies": { "@decentralized-identity/ion-tools": "1.0.6", - "@tbd54566975/dwn-sdk-js": "0.0.26", + "@tbd54566975/dwn-sdk-js": "0.0.30-unstable-2023-04-15-149c713", "cross-fetch": "3.1.5", "ed2curve": "0.3.0", + "readable-web-to-node-stream": "3.0.2", "tweetnacl": "1.0.3" } } diff --git a/src/Web5.js b/src/Web5.js index 1f6a6dd5b..a605f5f81 100644 --- a/src/Web5.js +++ b/src/Web5.js @@ -1,10 +1,10 @@ -import { Web5DID } from './did/Web5DID.js'; -import { Web5DWN } from './dwn/Web5DWN.js'; -import { AppTransport } from './transport/AppTransport.js'; -import { HTTPTransport } from './transport/HTTPTransport.js'; -import { isUnsignedMessage, parseURL } from './utils.js'; +import { Web5Did } from './did/web5-did.js'; +import { Web5Dwn } from './dwn/web5-dwn.js'; +import { AppTransport } from './transport/app-transport.js'; +import { HttpTransport } from './transport/http-transport.js'; +import { isUnsignedMessage, parseUrl } from './utils.js'; -class Web5 extends EventTarget { +export class Web5 extends EventTarget { #dwn; #did; #transports; @@ -12,12 +12,12 @@ class Web5 extends EventTarget { constructor(options = { }) { super(); - this.#dwn = new Web5DWN(this, options?.dwn); - this.#did = new Web5DID(this); + this.#dwn = new Web5Dwn(this, options?.dwn); + this.#did = new Web5Did(this); this.#transports = { app: new AppTransport(this), - http: new HTTPTransport(this), - https: new HTTPTransport(this), + http: new HttpTransport(this), + https: new HttpTransport(this), }; } @@ -54,10 +54,10 @@ class Web5 extends EventTarget { } // TODO: Is this sufficient or might we improve how the calling app can respond by initiating a connect/re-connect flow? - return { status: { code: 422, detail: 'Local keys not available and remote agent not connected' } }; + return { status: { code: 401, detail: 'Local keys not available and remote agent not connected' } }; } - message = await this.#createSignedMessage(resolvedAuthor, message, data); + message = await this.#createSignedMessage(author, resolvedAuthor, message, data); } const resolvedTarget = await this.#did.resolve(target); @@ -71,18 +71,19 @@ class Web5 extends EventTarget { if (dwnNodes) { return this.#send(dwnNodes, { author, data, message, target }); } - return { status: { code: 422, detail: 'No DWN endpoints present in DID document. Request cannot be sent.' } }; + return { status: { code: 400, detail: 'No DWN endpoints present in DID document. Request cannot be sent.' } }; } - return { status: { code: 422, detail: 'Target DID could not be resolved' } }; + return { status: { code: 400, detail: 'Target DID could not be resolved' } }; } - async #createSignedMessage(resolvedAuthor, message, data) { - const authorizationSignatureInput = this.#dwn.SDK.Jws.createSignatureInput({ - keyId: resolvedAuthor.did + '#key-1', - keyPair: resolvedAuthor.keys, + async #createSignedMessage(author, resolvedAuthor, message, data) { + const keyId = '#dwn'; + const authorizationSignatureInput = this.#dwn.sdk.Jws.createSignatureInput({ + keyId: author + keyId, + keyPair: resolvedAuthor.keys[keyId].keyPair, }); - const signedMessage = await this.#dwn.SDK[message.interface + message.method].create({ + const signedMessage = await this.#dwn.sdk[message.interface + message.method].create({ ...message, authorizationSignatureInput, data, @@ -108,25 +109,29 @@ class Web5 extends EventTarget { * @param {*} request.data - The message data (if any). * @param {object} request.message - The DWeb message. * @param {string} request.target - The DID to send the message to. - * @returns Promise + * @returns {Promise} */ async #send(endpoints, request) { - let response; + let response, message = {}; for (let endpoint of endpoints) { try { - const url = parseURL(endpoint); + const url = parseUrl(endpoint); response = await this.#transports[url?.protocol?.slice(0, -1)]?.send(url.href, request); } catch (error) { - console.log(error); + console.error(error); // Intentionally ignore exception and try the next endpoint. } if (response) break; // Stop looping and return after the first endpoint successfully responds. } - return response ?? { status: { code: 503, detail: 'Service Unavailable' } }; + if (!isUnsignedMessage(request.message)) { + // If the message is signed return the `descriptor`, and if present, `recordId`. + const { recordId = null, descriptor } = request.message.message; + message = { recordId, descriptor }; + } + + response ??= { status: { code: 503, detail: 'Service Unavailable' } }; + + return { message, ...response }; } } - -export { - Web5, -}; diff --git a/src/did/connect/connect.js b/src/did/connect/connect.js index 3d7305e68..14a5694fe 100644 --- a/src/did/connect/connect.js +++ b/src/did/connect/connect.js @@ -1,9 +1,9 @@ -import { DIDConnectRPCMethods, DIDConnectStep, JSONRPCErrorCodes, findWebSocketListener } from './utils.js'; +import { DidConnectRpcMethods, DidConnectStep, JsonRpcErrorCodes, findWebSocketListener } from './utils.js'; import { WebSocketClient } from './ws-client.js'; -import { parseJSON } from '../../utils.js'; -import { LocalStorage } from '../../storage/LocalStorage.js'; +import { parseJson } from '../../utils.js'; +import { LocalStorage } from '../../storage/local-storage.js'; -export class DIDConnect { +export class DidConnect { #web5; #client = null; @@ -11,7 +11,6 @@ export class DIDConnect { #permissionsRequests = []; - // TEMP // TODO: Replace this once the DID Manager and Keystore have been implemented #storage = null; #didStoreName = null; @@ -77,17 +76,17 @@ export class DIDConnect { async #initiateWeb5Client() { // Handler that will be used to step through the DIDConnect process phases const handleMessage = async (event) => { - const rpcMessage = parseJSON(event.data); + const rpcMessage = parseJson(event.data); switch (connectStep) { - case DIDConnectStep.Initiation: { + case DidConnectStep.Initiation: { // The Client App initiates the DIDConnect process, so no messages from the Provider are expected until the Verification step console.warn('Unexpected message received before Web5 Client was ready'); break; } - case DIDConnectStep.Verification: { + case DidConnectStep.Verification: { const verificationResult = rpcMessage?.result; // Encrypted PIN challenge received from DIDConnect Provider if (verificationResult?.ok) { @@ -95,18 +94,18 @@ export class DIDConnect { const pinBytes = await this.web5.did.decrypt({ did: this.#did.id, payload: verificationResult.payload, - privateKey: this.#did.keys[0].keypair.privateKeyJwk.d, // TODO: Remove once a keystore has been implemented + privateKey: this.#did.keys[0].keyPair.privateKeyJwk.d, // TODO: Remove once a keystore has been implemented }); - const pin = this.web5.dwn.SDK.Encoder.bytesToString(pinBytes); + const pin = this.web5.dwn.sdk.Encoder.bytesToString(pinBytes); // Emit event notifying the DWA that the PIN can be displayed to the end user this.web5.dispatchEvent(new CustomEvent('challenge', { detail: { pin } })); // Advance DIDConnect to Delegation and wait for challenge response from DIDConect Provider - connectStep = DIDConnectStep.Delegation; + connectStep = DidConnectStep.Delegation; // Send queued PermissionsRequest to Provider. - this.#client.sendRequest(DIDConnectRPCMethods[connectStep], { message: this.#permissionsRequests.pop() }); + this.#client.sendRequest(DidConnectRpcMethods[connectStep], { message: this.#permissionsRequests.pop() }); } else { // TODO: Remove socket listeners, destroy socket, destroy this.#client, and emit error to notify user of app @@ -114,16 +113,15 @@ export class DIDConnect { break; } - case DIDConnectStep.Delegation: { + case DidConnectStep.Delegation: { const delegationResult = rpcMessage?.result; // Success if (delegationResult?.ok) { const authorizedDid = delegationResult?.message?.grantedBy; - // Register DID now that the connection was authorized - await this.web5.did.register({ + // Set Managed DID now that the connection was authorized + await this.web5.did.manager.set(authorizedDid, { connected: true, - did: authorizedDid, endpoint: `http://localhost:${this.#client.port}/dwn`, }); @@ -157,10 +155,10 @@ export class DIDConnect { this.#client = null; const { code = undefined, message = undefined } = delegationError; - if (code === JSONRPCErrorCodes.Unauthorized) { + if (code === JsonRpcErrorCodes.Unauthorized) { // Emit event notifying the DWA that the connection request was denied this.web5.dispatchEvent(new CustomEvent('denied', { detail: { message: message } })); - } else if (code === JSONRPCErrorCodes.Forbidden) { + } else if (code === JsonRpcErrorCodes.Forbidden) { // Emit event notifying the DWA that this app has been blocked from connecting this.web5.dispatchEvent(new CustomEvent('blocked', { detail: { message: message } })); } @@ -168,27 +166,27 @@ export class DIDConnect { // Reached terminal DID Connect state where connection was either authorized/denied/block // Reset DID Connect step to be ready for any future reconnects/switch account/change permission requests - connectStep = DIDConnectStep.Initiation; + connectStep = DidConnectStep.Initiation; break; } } }; - let connectStep = DIDConnectStep.Initiation; + let connectStep = DidConnectStep.Initiation; // Pre-Flight Check: Is the Web5 Client already connected to the Provider? If NO, try to connect. let connectedToProvider = this.#alreadyConnected() || await this.#connectWeb5Provider(); if (!connectedToProvider) return; - + // Start listening for messages from the DIDConnect Provider this.#client.addEventListener('message', handleMessage); // Send a request to the agent initiating the DIDConnect process - this.#client.sendRequest(DIDConnectRPCMethods.Initiation); + this.#client.sendRequest(DidConnectRpcMethods.Initiation); // Advance DIDConnect to Verification and wait for encrypted challenge PIN from DIDConnect Provider - connectStep = DIDConnectStep.Verification; + connectStep = DidConnectStep.Verification; } @@ -244,7 +242,7 @@ export class DIDConnect { if (this.#did === null) throw new Error('Unexpected state: DID data and configuration should have already been initialized'); // Dynamically generate DID Connect path in case origin has changed. - const encodedOrigin = this.#web5.dwn.SDK.Encoder.stringToBase64Url(location.origin); + const encodedOrigin = this.#web5.dwn.sdk.Encoder.stringToBase64Url(location.origin); const connectPath = `didconnect/${this.#did.id}/${encodedOrigin}`; let socket, startPort, endPort, userInitiatedAction, host; diff --git a/src/did/connect/utils.js b/src/did/connect/utils.js index 507a4c250..3567acf2f 100644 --- a/src/did/connect/utils.js +++ b/src/did/connect/utils.js @@ -1,12 +1,12 @@ -import { parseJSON, triggerProtocolHandler } from '../../utils.js'; +import { parseJson } from '../../utils.js'; -export const DIDConnectRPCMethods = { +export const DidConnectRpcMethods = { Ready: 'didconnect.ready', Initiation: 'didconnect.initiation', Delegation: 'didconnect.delegation', }; -export const JSONRPCErrorCodes = { +export const JsonRpcErrorCodes = { // JSON-RPC 2.0 pre-defined errors InvalidRequest: -32600, MethodNotFound: -32601, @@ -20,7 +20,7 @@ export const JSONRPCErrorCodes = { Forbidden: -50403, // equivalent to HTTP Status 403 }; -export const DIDConnectStep = { +export const DidConnectStep = { Initiation: 'Initiation', Verification: 'Verification', Delegation: 'Delegation', @@ -62,8 +62,8 @@ export async function findWebSocketListener( clearListeners(socket); // Resolve and complete connection only if the expected message is received from the agent. - const message = parseJSON(event.data); - if (message?.method === DIDConnectRPCMethods.Ready) { + const message = parseJson(event.data); + if (message?.method === DidConnectRpcMethods.Ready) { // Resolve and return the open socket back to the caller resolve(socket); } else { @@ -103,4 +103,12 @@ export async function findWebSocketListener( } } } -} \ No newline at end of file +} + +export async function triggerProtocolHandler(url) { + let form = document.createElement('form'); + form.action = url; + document.body.append(form); + form.submit(); + form.remove(); +} diff --git a/src/did/connect/ws-client.js b/src/did/connect/ws-client.js index 391437e9f..49f49a329 100644 --- a/src/did/connect/ws-client.js +++ b/src/did/connect/ws-client.js @@ -1,5 +1,3 @@ -import { parseJSON } from '../../utils.js'; - export class WebSocketClient { #port; #requestID = 0; diff --git a/src/did/crypto/x25519-xsalsa20-poly1305.js b/src/did/crypto/x25519-xsalsa20-poly1305.js index c618a9165..aea0d7aba 100644 --- a/src/did/crypto/x25519-xsalsa20-poly1305.js +++ b/src/did/crypto/x25519-xsalsa20-poly1305.js @@ -1,7 +1,7 @@ import nacl from 'tweetnacl'; import { Encoder } from '@tbd54566975/dwn-sdk-js'; -import { ed25519PrivateKeyToX25519, ed25519PublicKeyToX25519, verificationMethodToPublicKeyBytes } from '../../did/didUtils.js'; +import { ed25519PrivateKeyToX25519, ed25519PublicKeyToX25519, verificationMethodToPublicKeyBytes } from '../../did/utils.js'; import { bytesToObject, objectValuesBase64UrlToBytes, objectValuesBytesToBase64Url } from '../../utils.js'; export class X25519Xsalsa20Poly1305 { @@ -69,7 +69,7 @@ export class X25519Xsalsa20Poly1305 { // Convert recipient's Ed25519 public key to X25519 const recipientDHPublicKey = ed25519PublicKeyToX25519(recipientPublicKey); - // Generate ephemeral keypair + // Generate ephemeral keyPair const ephemeralKeyPair = nacl.box.keyPair(); // Generate new nonce for every operation diff --git a/src/did/manager.js b/src/did/manager.js new file mode 100644 index 000000000..7903f4e6b --- /dev/null +++ b/src/did/manager.js @@ -0,0 +1,22 @@ +export async function clear(store) { + if (store){ + store.clear(); + } +} + +export async function exists(id, store) { + const value = await store.get(id); + return value !== undefined; +} + +export async function get(id, store) { + return store.get(id); +} + +export async function remove(id, store) { + store.remove(id); +} + +export async function set(id, value, store) { + store.set(id, value); +} \ No newline at end of file diff --git a/src/did/methods/ion.js b/src/did/methods/ion.js index f69653d04..42a0c74ea 100644 --- a/src/did/methods/ion.js +++ b/src/did/methods/ion.js @@ -6,9 +6,9 @@ const didIonResolver = new DidIonResolver(); async function create(options = { }){ options.keys ||= [ { - id: 'key-1', + id: 'dwn', type: 'JsonWebKey2020', - keypair: await generateKeyPair(), + keyPair: await generateKeyPair(), purposes: ['authentication'], }, ]; @@ -17,8 +17,8 @@ async function create(options = { }){ content: { publicKeys: options.keys.map(key => { let pubkey = Object.assign({ }, key); - pubkey.publicKeyJwk = key.keypair.publicJwk; - delete pubkey.keypair; + pubkey.publicKeyJwk = key.keyPair.publicJwk; + delete pubkey.keyPair; return pubkey; }), ...(options.services && { services: options.services }), diff --git a/src/did/methods/key.js b/src/did/methods/key.js index ac645c69a..bd6ae1649 100644 --- a/src/did/methods/key.js +++ b/src/did/methods/key.js @@ -7,7 +7,7 @@ import { MULTICODEC_ED25519_PUB_HEADER, MULTICODEC_X25519_PUB_HEADER, createJWK, -} from '../didUtils.js'; +} from '../utils.js'; const didKeyResolver = new DidKeyResolver(); diff --git a/src/did/didUtils.js b/src/did/utils.js similarity index 97% rename from src/did/didUtils.js rename to src/did/utils.js index 859ca0250..0542cf320 100644 --- a/src/did/didUtils.js +++ b/src/did/utils.js @@ -33,7 +33,7 @@ export const DID_VERIFICATION_RELATIONSHIPS = [ * @param {string} options.crv Cryptographic curve * @param {Uint8Array} options.publicKey Public key bytes * @param {Uint8Array} options.privateKey Private key bytes - * @returns {{ id: string, type: string, controller: string, keypair: Object}} + * @returns {{ id: string, type: string, controller: string, keyPair: Object}} */ export async function createJWK(options) { const { id, crv, kty, kid, publicKey, privateKey } = options; @@ -41,16 +41,16 @@ export async function createJWK(options) { id: `${id}#${kid}`, type: 'JsonWebKey2020', controller: id, - keypair: {}, + keyPair: {}, }; const jwk = { crv, kid, kty }; - jsonWebKey.keypair.publicKeyJwk = { ...jwk }; - jsonWebKey.keypair.publicKeyJwk.x = Encoder.bytesToBase64Url(publicKey); + jsonWebKey.keyPair.publicKeyJwk = { ...jwk }; + jsonWebKey.keyPair.publicKeyJwk.x = Encoder.bytesToBase64Url(publicKey); - jsonWebKey.keypair.privateKeyJwk = { ...jsonWebKey.keypair.publicKeyJwk }; - jsonWebKey.keypair.privateKeyJwk.d = Encoder.bytesToBase64Url(privateKey); + jsonWebKey.keyPair.privateKeyJwk = { ...jsonWebKey.keyPair.publicKeyJwk }; + jsonWebKey.keyPair.privateKeyJwk.d = Encoder.bytesToBase64Url(privateKey); return jsonWebKey; } diff --git a/src/did/Web5DID.js b/src/did/web5-did.js similarity index 80% rename from src/did/Web5DID.js rename to src/did/web5-did.js index c02723690..6f96b8fbe 100644 --- a/src/did/Web5DID.js +++ b/src/did/web5-did.js @@ -1,19 +1,20 @@ import { Encoder } from '@tbd54566975/dwn-sdk-js'; -import { DIDConnect } from './connect/connect.js'; +import { DidConnect } from './connect/connect.js'; import * as CryptoCiphers from './crypto/ciphers.js'; +import * as DidManager from './manager.js'; import * as Methods from './methods/methods.js'; -import * as DidUtils from './didUtils.js'; -import { MemoryStorage } from '../storage/MemoryStorage.js'; +import * as DidUtils from './utils.js'; +import { MemoryStorage } from '../storage/memory-storage.js'; import { pascalToKebabCase } from '../utils.js'; -class Web5DID { +export class Web5Did { #cryptoCiphers = {}; #didConnect; #web5; - #registeredDIDs = new MemoryStorage(); - #resolvedDIDs = new MemoryStorage(); + #resolvedDids = new MemoryStorage(); + didStore = new MemoryStorage(); constructor(web5) { this.#web5 = web5; @@ -23,8 +24,8 @@ class Web5DID { this.#cryptoCiphers[cipherName] = new CryptoCiphers[cipher](this.web5); } - this.#didConnect = new DIDConnect(web5); - // Bind functions to the instance of DIDConnect + this.#didConnect = new DidConnect(web5); + // Bind functions to the instance of DidConnect this.#didConnect.connect = this.#didConnect.connect.bind(this.#didConnect); this.#didConnect.permissionsRequest = this.#didConnect.permissionsRequest.bind(this.#didConnect); } @@ -37,8 +38,18 @@ class Web5DID { return this.#didConnect.permissionsRequest; } + get manager() { + return { + clear: (...args) => DidManager.clear(...args, this.didStore), + exists: (...args) => DidManager.exists(...args, this.didStore), + get: (...args) => DidManager.get(...args, this.didStore), + remove: (...args) => DidManager.remove(...args, this.didStore), + set: (...args) => DidManager.set(...args, this.didStore), + }; + } + get util() { - return this.#util; + return DidUtils; } get web5() { @@ -92,36 +103,23 @@ class Web5DID { return api.encrypt(options); } - async register(data) { - await this.#registeredDIDs.set(data.did, { - connected: data.connected, - did: data.did, // TODO: Consider removing if createAndSignMessage() no longer requires for Key ID - endpoint: data.endpoint, - keys: data.keys, - }); - } - async sign(method, options = { }) { const api = await this.#getMethodAPI(method); return api.sign(options); } - async unregister(did) { - await this.#registeredDIDs.delete(did); - } - async verify(method, options = { }) { const api = await this.#getMethodAPI(method); return api.verify(options); } async resolve(did, options = { }) { - const registered = await this.#registeredDIDs.get(did); - if (registered) { - return registered; + const managed = await this.manager.get(did); + if (managed) { + return managed; } - const resolved = await this.#resolvedDIDs.get(did); + const resolved = await this.#resolvedDids.get(did); if (resolved) { return resolved; } @@ -130,8 +128,8 @@ class Web5DID { const result = await api.resolve(did); if (options.cache) { - // store separately in case the DID is `register` after `resolve` was called - await this.#resolvedDIDs.set(did, result, { + // store separately in case the DID is `managed` after `resolve` was called. + await this.#resolvedDids.set(did, result, { timeout: 1000 * 60 * 60, // 1hr }); } @@ -174,13 +172,4 @@ class Web5DID { if (!api) throw `Unsupported cryptographic cipher: ${name}`; return api; } - - /** - * Utility functions for working with DIDs - */ - #util = { ...DidUtils }; } - -export { - Web5DID, -}; diff --git a/src/dwn/dwn-utils.js b/src/dwn/dwn-utils.js new file mode 100644 index 000000000..5e7a4462c --- /dev/null +++ b/src/dwn/dwn-utils.js @@ -0,0 +1,7 @@ +/** + * Uses duck typing to determine whether the stream is a web browser ReadableStream + * or a Node.js Readable stream. + */ +export function isReadableWebStream(stream) { + return typeof stream._read !== 'function'; +} diff --git a/src/dwn/interface/Records.js b/src/dwn/interface/Records.js deleted file mode 100644 index b2461896b..000000000 --- a/src/dwn/interface/Records.js +++ /dev/null @@ -1,37 +0,0 @@ -import { Interface } from './Interface.js'; -import { dataToBytes } from '../../utils.js'; - -class Records extends Interface { - constructor(dwn) { - super(dwn, 'Records'); - } - - async delete(target, request) { - return this.send('Delete', target, request); - } - - async read(target, request) { - return this.send('Read', target, request); - } - - async query(target, request) { - return this.send('Query', target, request); - } - - async write(target, request) { - // Convert string/object data to bytes before further processing. - const { dataBytes, dataFormat } = dataToBytes(request.data, request.message.dataFormat); - return this.send('Write', target, { - ...request, - data: dataBytes, - message: { - ...request.message, - dataFormat, - }, - }); - } -} - -export { - Records, -}; diff --git a/src/dwn/interface/Interface.js b/src/dwn/interfaces/interface.js similarity index 87% rename from src/dwn/interface/Interface.js rename to src/dwn/interfaces/interface.js index 1f3b009eb..ac57e99ca 100644 --- a/src/dwn/interface/Interface.js +++ b/src/dwn/interfaces/interface.js @@ -1,4 +1,4 @@ -class Interface { +export class Interface { #dwn; #name; @@ -7,6 +7,10 @@ class Interface { this.#name = name; } + get dwn() { + return this.#dwn; + } + // TODO: Remove this once Permissions implemented in dwn-sdk-js get permissionsRequest() { return this.#dwn.web5.did.permissionsRequest; @@ -23,7 +27,3 @@ class Interface { }); } } - -export { - Interface, -}; diff --git a/src/dwn/interface/Permissions.js b/src/dwn/interfaces/permissions.js similarity index 90% rename from src/dwn/interface/Permissions.js rename to src/dwn/interfaces/permissions.js index d5f53e470..ca2a9694c 100644 --- a/src/dwn/interface/Permissions.js +++ b/src/dwn/interfaces/permissions.js @@ -1,5 +1,5 @@ import { v4 as uuid } from 'uuid'; -import { Interface } from './Interface.js'; +import { Interface } from './interface.js'; class Permissions extends Interface { constructor(dwn) { diff --git a/src/dwn/interface/Protocols.js b/src/dwn/interfaces/protocols.js similarity index 87% rename from src/dwn/interface/Protocols.js rename to src/dwn/interfaces/protocols.js index a22e2d298..b08e2031e 100644 --- a/src/dwn/interface/Protocols.js +++ b/src/dwn/interfaces/protocols.js @@ -1,4 +1,4 @@ -import { Interface } from './Interface.js'; +import { Interface } from './interface.js'; class Protocols extends Interface { constructor(dwn) { diff --git a/src/dwn/interfaces/records.js b/src/dwn/interfaces/records.js new file mode 100644 index 000000000..766619c23 --- /dev/null +++ b/src/dwn/interfaces/records.js @@ -0,0 +1,103 @@ +import { DwnConstant } from '@tbd54566975/dwn-sdk-js'; + +import { Interface } from './interface.js'; +import { Record } from '../models/record.js'; +import { dataToBytes } from '../../utils.js'; + +export class Records extends Interface { + constructor(dwn) { + super(dwn, 'Records'); + } + + get create() { + return this.write; + } + + async createFrom(target, request) { + const { author: inheritedAuthor, target: _, ...inheritedProperties } = request.record.toJSON(); + + // If `data` is being updated then `dataCid` and `dataSize` must not be present. + if (request?.data !== undefined) { + delete inheritedProperties.dataCid; + delete inheritedProperties.dataSize; + } + + // If `published` is set to false, ensure that `datePublished` is undefined. Otherwise, DWN SDK's schema validation + // will throw an error if `published` is false but `datePublished` is set. + if (request?.message?.published === false && inheritedProperties?.datePublished !== undefined) { + delete inheritedProperties.datePublished; + delete inheritedProperties.published; + } + + // If the request changes the `author` or message `descriptor` then the deterministic `recordId` will change. + // As a result, we will discard the `recordId` if either of these changes occur. + if ((request?.message && Object.keys(request.message).length > 0) + || (request?.author && request.author !== inheritedAuthor)) { + delete inheritedProperties.recordId; + } + + return this.write(target, { + author: request?.author || inheritedAuthor, + data: request?.data, + message: { + ...inheritedProperties, + ...request.message, + }, + }); + } + + async delete(target, request) { + const response = await this.send('Delete', target, request); + return response; + } + + async read(target, request) { + const response = await this.send('Read', target, request); + + let record; + if (response?.record) { + record = new Record(this.dwn, { ...response.record, target, author: request.author }); + } + + return { ...response, record }; + } + + async query(target, request) { + const response = await this.send('Query', target, request); + + const entries = []; + response.entries.forEach(entry => { + entries.push(new Record(this.dwn, { ...entry, target, author: request.author })); + }); + return { ...response, entries }; + } + + async write(target, request) { + let dataBytes, dataFormat; + if (request?.data) { + // If `data` is specified, convert string/object data to bytes before further processing. + ({ dataBytes, dataFormat } = dataToBytes(request.data, request.message.dataFormat)); + } else { + // If not, `dataFormat` must be specified in the request message. + dataFormat = request.message.dataFormat; + } + + const response = await this.send('Write', target, { + ...request, + data: dataBytes, + message: { + ...request.message, + dataFormat, + }, + }); + + let record; + if (response?.message) { + // Include data if `dataSize` is less than DWN 'max data size allowed to be encoded'. + const encodedData = (response.message?.descriptor?.dataSize <= DwnConstant.maxDataSizeAllowedToBeEncoded) ? dataBytes : null; + + record = new Record(this.dwn, { ...response.message, encodedData, target, author: request.author }); + } + return { ...response, record }; + } +} diff --git a/src/dwn/models/record.js b/src/dwn/models/record.js new file mode 100644 index 000000000..4395b0618 --- /dev/null +++ b/src/dwn/models/record.js @@ -0,0 +1,244 @@ +import { DataStream, DwnConstant, Encoder } from '@tbd54566975/dwn-sdk-js'; +import { ReadableWebToNodeStream } from 'readable-web-to-node-stream'; + +import { isReadableWebStream } from '../dwn-utils.js'; + +export class Record { + #dwn; + + #author; + #contextId; + #descriptor; + #recordId; + #target; + + #encodedData = null; + #isFrozen = false; + #readableStream = null; + + constructor(dwn, options = { }) { + this.#dwn = dwn; + + // RecordsWriteMessage properties. + const { author, contextId = undefined, descriptor, recordId = null, target } = options; + this.#contextId = contextId; + if (descriptor?.data) delete descriptor.data; + this.#descriptor = descriptor ?? { }; + this.#recordId = recordId; + + // Store the target and author DIDs that were used to create the message to use for subsequent reads, etc. + this.#author = author; + this.#target = target; + + // If the record `dataSize is less than the DwnConstant.maxDataSizeAllowedToBeEncoded value, + // then an `encodedData` property will be present. + this.#encodedData = options?.encodedData ?? null; + + // If the record was created from a RecordsRead reply then it will have a `data` property. + if (options?.data) { + // this.#readableStream = isReadableWebStream(options.data) ? toIsomorphicReadableStream(options.data) : options.data; + this.#readableStream = isReadableWebStream(options.data) ? new ReadableWebToNodeStream(options.data) : options.data; + } + } + + // Mutable Web5 Record Class properties. + get author() { return this.#author; } + get isFrozen() { return this.#isFrozen; } + get target() { return this.#target; } + set author(author) { this.#author = author; } + set target(target) { this.#target = target; } + + // Immutable DWN Record properties. + get id() { return this.#recordId; } + get contextId() { return this.#contextId; } + get dataFormat() { return this.#descriptor?.dataFormat; } + get dateCreated() { return this.#descriptor?.dateCreated; } + get interface() { return this.#descriptor?.interface; } + get method() { return this.#descriptor?.method; } + get parentId() { return this.#descriptor?.parentId; } + get protocol() { return this.#descriptor?.protocol; } + get recipient() { return this.#descriptor?.recipient; } + get schema() { return this.#descriptor?.schema; } + + // Mutable DWN Record properties. + get dataCid() { return this.#descriptor?.dataCid; } + get dataSize() { return this.#descriptor?.dataSize; } + get dateModified() { return this.#descriptor?.dateModified; } + get datePublished() { return this.#descriptor?.datePublished; } + get published() { return this.#descriptor?.published; } + + /** + * Data handling. + */ + + get data() { + if (!this.#encodedData && !this.#readableStream) { + // `encodedData` will be set if `dataSize` <= DwnConstant.maxDataSizeAllowedToBeEncoded. (10KB as of April 2023) + // `readableStream` will be set if Record was instantiated from a RecordsRead reply. + // If neither of the above are true, then the record must be fetched from the DWN. + this.#readableStream = this.#dwn.records.read(this.#target, { author: this.#author, message: { recordId: this.#recordId } }) + .then((response) => response.record ) + .then((record) => { return record.data; }); + } + + if (typeof this.#encodedData === 'string') { + // If `encodedData` is set, then it is expected that: + // `dataSize` <= DwnConstant.maxDataSizeAllowedToBeEncoded (10KB as of April 2023) + // type is Uint8Array bytes if the Record object was instantiated from a RecordsWrite response + // type is Base64 URL encoded string if the Record object was instantiated from a RecordsQuery response + // If it is a string, we need to Base64 URL decode to bytes + this.#encodedData = Encoder.base64UrlToBytes(this.#encodedData); + } + + const self = this; // Capture the context of the `Record` instance. + const dataObj = { + async json() { + if (self.#encodedData) return this.text().then(JSON.parse); + if (self.#readableStream) return this.text().then(JSON.parse); + return null; + }, + async text() { + if (self.#encodedData) return Encoder.bytesToString(self.#encodedData); + if (self.#readableStream) return self.#readableStream.then(DataStream.toBytes).then(Encoder.bytesToString); + return null; + }, + async stream() { + if (self.#encodedData) return DataStream.fromBytes(self.#encodedData); + if (self.#readableStream) return self.#readableStream; + return null; + }, + then (...callbacks) { + return this.stream().then(...callbacks); + }, + catch(callback) { + return dataObj.then().catch(callback); + }, + }; + return dataObj; + } + + /** + * Record mutation methods. + */ + + async delete() { + if (this.isFrozen) throw new Error(`Error: Record with ID '${this.id}' was previously deleted.`); + + // Attempt to delete the record from the DWN. + const response = await this.#dwn.records.delete(this.#target, { author: this.#author, message: { recordId: this.#recordId } }); + + if (response.status.code === 202) { + // If the record was successfully deleted, freeze the instance to prevent further modifications. + this.#freezeRecord(); + } + + return response; + } + + async update(options = { }) { + if (this.isFrozen) throw new Error(`Error: Record with ID '${this.id}' was previously deleted.`); + + // Begin assembling update message. + let updateMessage = { ...this.#descriptor, ...options }; + + // If `data` is being updated then `dataCid` and `dataSize` must be undefined and the `data` property is passed as + // a top-level property to `web5.dwn.records.write()`. + let data; + if (options?.data !== undefined) { + delete updateMessage.dataCid; + delete updateMessage.dataSize; + data = options.data; + delete options.data; + } + + // Throw an error if an attempt is made to modify immutable properties. `data` has already been handled. + const mutableDescriptorProperties = ['dataCid', 'dataSize', 'dateModified', 'datePublished', 'published']; + Record.#verifyPermittedMutation(Object.keys(options), mutableDescriptorProperties); + + // If a new `dateModified` was not provided, remove it from the updateMessage to let the DWN SDK auto-fill. + // This is necessary because otherwise DWN SDK throws an Error 409 Conflict due to attempting to overwrite a record + // when the `dateModified` timestamps are identical. + if (options?.dateModified === undefined) { + delete updateMessage.dateModified; + } + + // If `published` is set to false, ensure that `datePublished` is undefined. Otherwise, DWN SDK's schema validation + // will throw an error if `published` is false but `datePublished` is set. + if (options?.published === false && updateMessage?.datePublished !== undefined) { + delete updateMessage.datePublished; + } + + // Set the record ID and context ID, if any. + updateMessage.recordId = this.#recordId; + updateMessage.contextId = this.#contextId; + + // Attempt to write the changes to mutable properties to the DWN. + const { message = null, record = null, status } = await this.#dwn.records.write(this.#target, { + author: this.#author, + data, + message: { + ...updateMessage, + }, + }); + + if (status.code === 202 && record) { + // Only update the local Record instance mutable properties if the record was successfully (over)written. + mutableDescriptorProperties.forEach(property => { + this.#descriptor[property] = record[property]; + }); + // Only update data if `dataSize` is less than DWN 'max data size allowed to be encoded'. + if (data !== undefined) { + this.#readableStream = (record.dataSize <= DwnConstant.maxDataSizeAllowedToBeEncoded) ? record.data : null; + this.#encodedData = null; // Clear `encodedData` in case it was previously set. + } + } + + return { message, status }; + } + + /** + * Utility methods. + */ + + /** + * Called by `JSON.stringify(...)` automatically. + */ + toJSON() { + return { + author: this.author, + target: this.target, + recordId: this.id, + contextId: this.contextId, + dataFormat: this.dataFormat, + dateCreated: this.dateCreated, + interface: this.interface, + method: this.method, + parentId: this.parentId, + protocol: this.protocol, + recipient: this.recipient, + schema: this.schema, + dataCid: this.dataCid, + dataSize: this.dataSize, + dateModified: this.dateModified, + datePublished: this.datePublished, + published: this.published, + }; + } + + #freezeRecord() { + this.#isFrozen = true; + } + + #unFreezeRecord() { + this.#isFrozen = false; + } + + static #verifyPermittedMutation(propertiesToMutate, mutableDescriptorProperties) { + propertiesToMutate.forEach(propertyName => { + if (!mutableDescriptorProperties.includes(propertyName)) { + throw new Error(`${propertyName} is an immutable property. Its value cannot be changed.`); + } + }); + return true; + } +} diff --git a/src/dwn/Web5DWN.js b/src/dwn/web5-dwn.js similarity index 67% rename from src/dwn/Web5DWN.js rename to src/dwn/web5-dwn.js index fe45a2713..faf38279a 100644 --- a/src/dwn/Web5DWN.js +++ b/src/dwn/web5-dwn.js @@ -1,13 +1,13 @@ -import * as SDK from '@tbd54566975/dwn-sdk-js'; +import * as Sdk from '@tbd54566975/dwn-sdk-js'; -import { Permissions } from './interface/Permissions.js'; -import { Protocols } from './interface/Protocols.js'; -import { Records } from './interface/Records.js'; +import { Permissions } from './interfaces/permissions.js'; +import { Protocols } from './interfaces/protocols.js'; +import { Records } from './interfaces/records.js'; import { createWeakSingletonAccessor } from '../utils.js'; -const sharedNode = createWeakSingletonAccessor(() => SDK.Dwn.create()); +const sharedNode = createWeakSingletonAccessor(() => Sdk.Dwn.create()); -class Web5DWN { +class Web5Dwn { #web5; #node; @@ -27,8 +27,8 @@ class Web5DWN { this.#records = new Records(this); } - get SDK() { - return SDK; + get sdk() { + return Sdk; } get web5() { @@ -53,5 +53,5 @@ class Web5DWN { } export { - Web5DWN, + Web5Dwn, }; diff --git a/src/main.js b/src/main.js index 803e648dc..4aaef5787 100644 --- a/src/main.js +++ b/src/main.js @@ -1 +1 @@ -export { Web5 } from './Web5.js'; +export { Web5 } from './web5.js'; diff --git a/src/storage/Storage.js b/src/storage/Storage.js index fcc91fee2..7548b581e 100644 --- a/src/storage/Storage.js +++ b/src/storage/Storage.js @@ -7,7 +7,7 @@ class Storage { throw 'subclass must override'; } - async delete(_key) { + async remove(_key) { throw 'subclass must override'; } diff --git a/src/storage/LocalStorage.js b/src/storage/local-storage.js similarity index 83% rename from src/storage/LocalStorage.js rename to src/storage/local-storage.js index e1ecbd208..697525f22 100644 --- a/src/storage/LocalStorage.js +++ b/src/storage/local-storage.js @@ -1,4 +1,4 @@ -import { Storage } from './Storage.js'; +import { Storage } from './storage.js'; class LocalStorage extends Storage { async get(key) { @@ -9,7 +9,7 @@ class LocalStorage extends Storage { localStorage.setItem(key, JSON.stringify(value)); } - async delete(key) { + async remove(key) { localStorage.removeItem(key); } diff --git a/src/storage/MemoryStorage.js b/src/storage/memory-storage.js similarity index 89% rename from src/storage/MemoryStorage.js rename to src/storage/memory-storage.js index a1d5f23fc..2a9f2cc2c 100644 --- a/src/storage/MemoryStorage.js +++ b/src/storage/memory-storage.js @@ -1,4 +1,4 @@ -import { Storage } from './Storage.js'; +import { Storage } from './storage.js'; class MemoryStorage extends Storage { #dataForKey = new Map; @@ -13,13 +13,13 @@ class MemoryStorage extends Storage { if (Number.isFinite(options?.timeout)) { const timeout = setTimeout(() => { - this.delete(key); + this.remove(key); }, options.timeout); this.#timeoutForKey.set(key, timeout); } } - async delete(key) { + async remove(key) { this.#dataForKey.delete(key); clearTimeout(this.#timeoutForKey.get(key)); diff --git a/src/storage/storage.js b/src/storage/storage.js new file mode 100644 index 000000000..7548b581e --- /dev/null +++ b/src/storage/storage.js @@ -0,0 +1,21 @@ +class Storage { + async get(_key) { + throw 'subclass must override'; + } + + async set(_key, _value) { + throw 'subclass must override'; + } + + async remove(_key) { + throw 'subclass must override'; + } + + async clear() { + throw 'subclass must override'; + } +} + +export { + Storage, +}; diff --git a/src/transport/AppTransport.js b/src/transport/app-transport.js similarity index 80% rename from src/transport/AppTransport.js rename to src/transport/app-transport.js index 2ea3d25dd..b6ea5e07e 100644 --- a/src/transport/AppTransport.js +++ b/src/transport/app-transport.js @@ -1,8 +1,8 @@ import { DataStream } from '@tbd54566975/dwn-sdk-js'; -import { Transport } from './Transport.js'; +import { Transport } from './transport.js'; -class AppTransport extends Transport { +export class AppTransport extends Transport { async encodeData(data) { return DataStream.fromBytes(data); } @@ -17,7 +17,3 @@ class AppTransport extends Transport { return node.processMessage(request.target, request.message.message, encodedData); } } - -export { - AppTransport, -}; diff --git a/src/transport/HTTPTransport.js b/src/transport/http-transport.js similarity index 59% rename from src/transport/HTTPTransport.js rename to src/transport/http-transport.js index e4d202382..3652f797a 100644 --- a/src/transport/HTTPTransport.js +++ b/src/transport/http-transport.js @@ -1,7 +1,7 @@ import crossFetch from 'cross-fetch'; import { Encoder } from '@tbd54566975/dwn-sdk-js'; -import { Transport } from './Transport.js'; +import { Transport } from './transport.js'; /** * Supports fetch in: browser, browser extensions, Node, and React Native. @@ -16,8 +16,9 @@ import { Transport } from './Transport.js'; */ const fetch = globalThis.fetch ?? crossFetch; -class HTTPTransport extends Transport { +export class HttpTransport extends Transport { ENCODED_MESSAGE_HEADER = 'DWN-MESSAGE'; + ENCODED_RESPONSE_HEADER = 'WEB5-RESPONSE'; async encodeMessage(message) { return Encoder.stringToBase64Url(JSON.stringify(message)); @@ -28,7 +29,7 @@ class HTTPTransport extends Transport { } async send(endpoint, request) { // override - return fetch(endpoint, { + const response = await fetch(endpoint, { method: 'POST', mode: 'cors', cache: 'no-cache', @@ -41,17 +42,21 @@ class HTTPTransport extends Transport { 'Content-Type': 'application/octet-stream', }, body: request.data, - }) - .then((response) => { - // Only resolve if response was successful (status of 200-299) - if (response.ok) { - return response; - } - return Promise.reject(response); - }); + }); + + if (!response.ok) { + throw new Error(`Fetch failed with status ${response.status}`); + } + + const web5ResponseHeader = response.headers.get(this.ENCODED_RESPONSE_HEADER); + if (web5ResponseHeader) { + // RecordsRead responses return `message` and `status` as header values, with a `data` ReadableStream in the body. + const { entries = null, message, record, status } = await this.decodeMessage(web5ResponseHeader); + return { entries, message, record: { data: response.body, ...record }, status }; + + } else { + // All other DWN responses return `entries`, `message`, and `status` as stringified JSON in the body. + return await response.json(); + } } } - -export { - HTTPTransport, -}; diff --git a/src/transport/transport.js b/src/transport/transport.js new file mode 100644 index 000000000..348a86c06 --- /dev/null +++ b/src/transport/transport.js @@ -0,0 +1,19 @@ +class Transport { + #web5; + + constructor(web5) { + this.#web5 = web5; + } + + get web5() { + return this.#web5; + } + + async send(_endpoint, _request) { + throw 'subclass must override'; + } +} + +export { + Transport, +}; diff --git a/src/types.js b/src/types.js new file mode 100644 index 000000000..b2e6a150b --- /dev/null +++ b/src/types.js @@ -0,0 +1,230 @@ +/** + * NOTE: These are temporary until Web5 JS can be converted to TypeScript and import the types from dwn-sdk-js. + */ + +/** + * Web5 JS type definitions. + */ + +/** + * @typedef {Object} Web5SendResponseMessage + * @property {ProtocolsConfigureDescriptor | ProtocolsQueryDescriptor | RecordsQueryDescriptor | RecordsReadDescriptor | RecordsWriteDescriptor} message + */ + +/** + * @typedef {MessageReplyOptions | Web5SendResponseMessage} Web5SendResponse + */ + +/** + * DWN SDK JS type definitions converted from TypeScript. + */ + +/** + * @typedef {Object} BaseMessage + * @property {Descriptor} descriptor + * @property {GeneralJws} authorization + */ + +/** + * @typedef {Object} Descriptor + * @property {string} interface + * @property {string} method + * @property {string} [dataCid] + * @property {number} [dataSize] + */ + +/** + * @typedef {Object} GeneralJws + * @property {string} payload + * @property {SignatureEntry[]} signatures + */ + +/** + * TODO: Readable isn't resolved. Expected solution is to import types from dwn-sdk-js once Web5 JS is converted to TS. + * @typedef {Object} MessageReplyOptions + * @property {Status} status + * @property {QueryResultEntry[]} [entries] + * @property {Readable} [data] + */ + +/** + * @typedef {Object} QueryResultEntry + * @property {Descriptor} descriptor + * @property {string} [encodedData] + */ + +/** + * @typedef {Object} SignatureEntry + * @property {string} protected + * @property {string} signature + */ + +/** + * @typedef {Object} Status + * @property {number} code + * @property {string} detail + */ + +/** + * @typedef {Object} ProtocolsConfigureDescriptor + * @property {DwnInterfaceName.Protocols} interface + * @property {DwnMethodName.Configure} method + * @property {string} dateCreated + * @property {string} protocol + * @property {ProtocolDefinition} definition + */ + +/** + * @typedef {Object} ProtocolDefinition + * @property {Object} labels + * @property {Object} records + */ + +/** + * @typedef {Object} ProtocolRuleSet + * @property {Object} [allow] + * @property {Object} [allow.anyone] + * @property {string[]} [allow.anyone.to] + * @property {Object} [allow.recipient] + * @property {string} [allow.recipient.of] + * @property {string[]} [allow.recipient.to] + * @property {Object} [records] + */ + +/** + * @typedef {BaseMessage & Object} ProtocolsConfigureMessage + * @property {ProtocolsConfigureDescriptor} descriptor + */ + +/** + * @typedef {Object} ProtocolsQueryDescriptor + * @property {DwnInterfaceName.Protocols} interface + * @property {DwnMethodName.Query} method + * @property {string} dateCreated + * @property {Object} [filter] + * @property {string} [filter.protocol] + */ + +/** + * @typedef {BaseMessage & Object} ProtocolsQueryMessage + * @property {ProtocolsQueryDescriptor} descriptor + */ + +/** + * @typedef {Object} RecordsWriteDescriptor + * @property {DwnInterfaceName.Records} interface + * @property {DwnMethodName.Write} method + * @property {string} [protocol] + * @property {string} recipient + * @property {string} [schema] + * @property {string} [parentId] + * @property {string} dataCid + * @property {number} dataSize + * @property {string} dateCreated + * @property {string} dateModified + * @property {boolean} [published] + * @property {string} [datePublished] + * @property {string} dataFormat + */ + +/** + * @typedef {BaseMessage & Object} RecordsWriteMessage + * @property {string} recordId + * @property {string} [contextId] + * @property {RecordsWriteDescriptor} descriptor + * @property {GeneralJws} [attestation] + */ + +/** + * @typedef {Object} RecordsQueryDescriptor + * @property {DwnInterfaceName.Records} interface + * @property {DwnMethodName.Query} method + * @property {string} dateCreated + * @property {RecordsQueryFilter} filter + * @property {DateSort} [dateSort] + */ + +/** + * @typedef {'Records'} DwnInterfaceName.Records + */ +/** + * @typedef {'Hooks'} DwnInterfaceName.Hooks + */ +/** + * @typedef {'Protocols'} DwnInterfaceName.Protocols + */ +/** + * @typedef {'Permissions'} DwnInterfaceName.Permissions + */ +/** + * @typedef {'Configure'} DwnMethodName.Configure + */ +/** + * @typedef {'Grant'} DwnMethodName.Grant + */ +/** + * @typedef {'Query'} DwnMethodName.Query + */ +/** + * @typedef {'Read'} DwnMethodName.Read + */ +/** + * @typedef {'Request'} DwnMethodName.Request + */ +/** + * @typedef {'Write'} DwnMethodName.Write + */ +/** + * @typedef {'Delete'} DwnMethodName.Delete + */ + +/** + * @typedef {Object} RecordsQueryFilter + * @property {string} [attester] + * @property {string} [recipient] + * @property {string} [protocol] + * @property {string} [contextId] + * @property {string} [schema] + * @property {string} [recordId] + * @property {string} [parentId] + * @property {string} [dataFormat] + * @property {RangeCriterion} [dateCreated] + */ + +/** + * @typedef {Object} RangeCriterion + * @property {string} [from] + * @property {string} [to] + */ + +/** + * @typedef {BaseMessage & Object} RecordsQueryMessage + * @property {RecordsQueryDescriptor} descriptor + */ + +/** + * @typedef {Object} RecordsReadMessage + * @property {GeneralJws} [authorization] + * @property {RecordsReadDescriptor} descriptor + */ + +/** + * @typedef {Object} RecordsReadDescriptor + * @property {DwnInterfaceName.Records} interface + * @property {DwnMethodName.Read} method + * @property {string} recordId + * @property {string} date + */ + +/** + * @typedef {BaseMessage & Object} RecordsDeleteMessage + * @property {RecordsDeleteDescriptor} descriptor + */ + +/** + * @typedef {Object} RecordsDeleteDescriptor + * @property {DwnInterfaceName.Records} interface + * @property {DwnMethodName.Delete} method + * @property {string} recordId + * @property {string} dateModified + */ diff --git a/src/utils.js b/src/utils.js index 468e989a3..598882f0b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,3 @@ -import nacl from 'tweetnacl'; import { Encoder } from '@tbd54566975/dwn-sdk-js'; const textDecoder = new TextDecoder(); @@ -34,7 +33,7 @@ function isEmptyObject(obj) { return false; } -function parseJSON(str) { +function parseJson(str) { try { return JSON.parse(str); } catch { @@ -42,7 +41,7 @@ function parseJSON(str) { } } -function parseURL(str) { +function parseUrl(str) { try { return new URL(str); } catch { @@ -111,33 +110,14 @@ const toType = (obj) => { return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); }; -async function triggerProtocolHandler(url) { - let form = document.createElement('form'); - form.action = url; - document.body.append(form); - form.submit(); - form.remove(); -} - -async function decodePin(data, secretKey) { - const { pin, nonce, publicKey } = data; - const encryptedPinBytes = Encoder.base64UrlToBytes(pin); - const nonceBytes = new TextEncoder().encode(nonce); - const publicKeyBytes = Encoder.base64UrlToBytes(publicKey); - const encodedPin = nacl.box.open(encryptedPinBytes, nonceBytes, publicKeyBytes, secretKey); - data.pin = new TextDecoder().decode(encodedPin); -} - export { createWeakSingletonAccessor, dataToBytes, - decodePin, isEmptyObject, isUnsignedMessage, objectValuesBase64UrlToBytes, objectValuesBytesToBase64Url, - parseJSON, - parseURL, + parseJson, + parseUrl, pascalToKebabCase, - triggerProtocolHandler, }; diff --git a/src/web5.js b/src/web5.js new file mode 100644 index 000000000..a605f5f81 --- /dev/null +++ b/src/web5.js @@ -0,0 +1,137 @@ +import { Web5Did } from './did/web5-did.js'; +import { Web5Dwn } from './dwn/web5-dwn.js'; +import { AppTransport } from './transport/app-transport.js'; +import { HttpTransport } from './transport/http-transport.js'; +import { isUnsignedMessage, parseUrl } from './utils.js'; + +export class Web5 extends EventTarget { + #dwn; + #did; + #transports; + + constructor(options = { }) { + super(); + + this.#dwn = new Web5Dwn(this, options?.dwn); + this.#did = new Web5Did(this); + this.#transports = { + app: new AppTransport(this), + http: new HttpTransport(this), + https: new HttpTransport(this), + }; + } + + get dwn() { + return this.#dwn; + } + + get did() { + return this.#did; + } + + get transports() { + return this.#transports; + } + + /** + * @param {string} target The DID to send the message to. + * @param {object} request - Object containing the request parameters. + * @param {string} request.author - The DID of the author of the message. + * @param {*} request.data - The message data (if any). + * @param {object} request.message - The DWeb message. + * @returns Promise + */ + async send(target, request) { + let { author, data, message } = request; + + if (isUnsignedMessage(message)) { + const resolvedAuthor = await this.#did.resolve(author); + + // If keys are not available to sign messages, transport the message to the specified agent. + if (!resolvedAuthor?.keys) { + if (resolvedAuthor?.connected) { + return this.#send([resolvedAuthor.endpoint], { author, data, message, target }); + } + + // TODO: Is this sufficient or might we improve how the calling app can respond by initiating a connect/re-connect flow? + return { status: { code: 401, detail: 'Local keys not available and remote agent not connected' } }; + } + + message = await this.#createSignedMessage(author, resolvedAuthor, message, data); + } + + const resolvedTarget = await this.#did.resolve(target); + + if (resolvedTarget?.connected) { + return this.#send([resolvedTarget.endpoint], { author, data, message, target }); + } else if (resolvedTarget?.didDocument) { + // Resolve the DWN endpoint(s) of the target and send using the endpoint's transport protocol (e.g., HTTP). + const dwnServices = await this.#did.getServices(target, { cache: true, type: 'DecentralizedWebNode' }); + const dwnNodes = dwnServices[0]?.serviceEndpoint?.nodes; + if (dwnNodes) { + return this.#send(dwnNodes, { author, data, message, target }); + } + return { status: { code: 400, detail: 'No DWN endpoints present in DID document. Request cannot be sent.' } }; + } + + return { status: { code: 400, detail: 'Target DID could not be resolved' } }; + } + + async #createSignedMessage(author, resolvedAuthor, message, data) { + const keyId = '#dwn'; + const authorizationSignatureInput = this.#dwn.sdk.Jws.createSignatureInput({ + keyId: author + keyId, + keyPair: resolvedAuthor.keys[keyId].keyPair, + }); + const signedMessage = await this.#dwn.sdk[message.interface + message.method].create({ + ...message, + authorizationSignatureInput, + data, + }); + delete signedMessage.data; + return signedMessage; + } + + /** + * Sends the message to one or more endpoint URIs + * + * If more than one endpoint is passed, each endpoint is tried serially until one succeeds or all fail. + * + * This strategy is used to account for cases like attempting to write large data streams to + * the DWN endpoints listed in a DID document. It would be inefficient to attempt to write data to + * multiple endpoints in parallel until the first one completes. Instead, we only try the next DWN if + * there is a failure. Additionally, per the DWN Specification, implementers SHOULD select from the + * Service Endpoint URIs in the nodes array in index order, so this function makes that approach easy. + * + * @param {string[]} endpoints - An array of one or more endpoints to send the message to. + * @param {object} request - Object containing the request parameters. + * @param {string} request.author - The DID of the author of the message. + * @param {*} request.data - The message data (if any). + * @param {object} request.message - The DWeb message. + * @param {string} request.target - The DID to send the message to. + * @returns {Promise} + */ + async #send(endpoints, request) { + let response, message = {}; + for (let endpoint of endpoints) { + try { + const url = parseUrl(endpoint); + response = await this.#transports[url?.protocol?.slice(0, -1)]?.send(url.href, request); + } catch (error) { + console.error(error); + // Intentionally ignore exception and try the next endpoint. + } + if (response) break; // Stop looping and return after the first endpoint successfully responds. + } + + if (!isUnsignedMessage(request.message)) { + // If the message is signed return the `descriptor`, and if present, `recordId`. + const { recordId = null, descriptor } = request.message.message; + message = { recordId, descriptor }; + } + + response ??= { status: { code: 503, detail: 'Service Unavailable' } }; + + return { message, ...response }; + } +} diff --git a/tests/data/didDocuments.js b/tests/data/did-documents.js similarity index 100% rename from tests/data/didDocuments.js rename to tests/data/did-documents.js diff --git a/tests/did/methods/ion.spec.js b/tests/did/methods/ion.spec.js index f860c6f9e..986167492 100644 --- a/tests/did/methods/ion.spec.js +++ b/tests/did/methods/ion.spec.js @@ -1,14 +1,14 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import { Web5DID } from '../../../src/did/Web5DID.js'; -import * as didDocuments from '../../data/didDocuments.js'; +import { Web5Did } from '../../../src/did/web5-did.js'; +import * as didDocuments from '../../data/did-documents.js'; -describe('Web5DID', async () => { +describe('Web5Did', async () => { let web5did; beforeEach(function () { - web5did = new Web5DID(); + web5did = new Web5Did(); }); before(function () { @@ -19,6 +19,30 @@ describe('Web5DID', async () => { this.clock.restore(); }); + describe('create', async () => { + it('should return one key when creating a did:ion DID', async () => { + const did = await web5did.create('ion'); + expect(did.keys).to.have.lengthOf(1); + }); + + it('should return one purpose, authentication, when creating a did:ion DID', async () => { + const did = await web5did.create('ion'); + expect(did.keys[0].purposes).to.have.lengthOf(1); + expect(did.keys[0].purposes[0]).to.equal('authentication'); + }); + + it('should return keys with a default `dwn` keyId when creating a did:ion DID', async () => { + const did = await web5did.create('ion'); + expect(did.keys[0].id).to.equal('dwn'); + }); + + it('should return keys in JWK format when creating a did:ion DID', async () => { + const did = await web5did.create('ion'); + expect(did.keys[0].keyPair).to.have.property('privateJwk'); + expect(did.keys[0].keyPair).to.have.property('publicJwk'); + }); + }); + describe('getDidDocument', async () => { it('should return a didDocument for a valid did:ion DID', async () => { sinon.stub(web5did, 'resolve').resolves(didDocuments.ion.oneVerificationMethodJwk); @@ -58,17 +82,16 @@ describe('Web5DID', async () => { }); describe('resolve', async () => { - it('should not call ion-tools resolve() when registered DID is cached', async () => { - // If registered DID isn't cached, the fetch() call to resolve over the network + it('should not call ion-tools resolve() when managed DID is cached', async () => { + // If managed DID isn't cached, the fetch() call to resolve over the network // will take far more than 10ms timeout, causing the test to fail. const did = 'did:ion:EiClkZMDxPKqC9c-umQfTkR8vvZ9JPhl_xLDI9Nfk38w5w'; const didData = { connected: true, - did: did, endpoint: 'http://localhost:55500', }; - web5did.register(didData); + web5did.manager.set(did, didData); const _ = await web5did.resolve(did); }).timeout(10); diff --git a/tests/did/methods/key.spec.js b/tests/did/methods/key.spec.js index f8d0f9250..2132c828a 100644 --- a/tests/did/methods/key.spec.js +++ b/tests/did/methods/key.spec.js @@ -1,14 +1,47 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import { Web5DID } from '../../../src/did/Web5DID.js'; -import * as didDocuments from '../../data/didDocuments.js'; +import { Web5Did } from '../../../src/did/web5-did.js'; +import * as didDocuments from '../../data/did-documents.js'; -describe('Web5DID', async () => { +describe('Web5Did', async () => { let web5did; beforeEach(function () { - web5did = new Web5DID(); + web5did = new Web5Did(); + }); + + describe('create', async () => { + it('should return two keys when creating a did:key DID', async () => { + const did = await web5did.create('key'); + expect(did.keys).to.have.lengthOf(2); + }); + + it('should return two keys with keyId in form `did:key:id#publicKeyKeyId` when creating a did:key DID', async () => { + const did = await web5did.create('key'); + expect(did.keys[0].id).to.equal(`${did.id}#${did.keys[0].keyPair.publicKeyJwk.kid}`); + expect(did.keys[1].id).to.equal(`${did.id}#${did.keys[1].keyPair.publicKeyJwk.kid}`); + }); + + it('should return keys in JWK format when creating a did:key DID', async () => { + const did = await web5did.create('key'); + expect(did.keys[0].type).to.equal('JsonWebKey2020'); + expect(did.keys[1].type).to.equal('JsonWebKey2020'); + expect(did.keys[0].keyPair).to.have.property('privateKeyJwk'); + expect(did.keys[0].keyPair).to.have.property('publicKeyJwk'); + expect(did.keys[1].keyPair).to.have.property('privateKeyJwk'); + expect(did.keys[1].keyPair).to.have.property('publicKeyJwk'); + }); + + it('should return first key using Ed25519 curve when creating a did:key DID', async () => { + const did = await web5did.create('key'); + expect(did.keys[0].keyPair.publicKeyJwk.crv).to.equal('Ed25519'); + }); + + it('should return second key using X25519 curve when creating a did:key DID', async () => { + const did = await web5did.create('key'); + expect(did.keys[1].keyPair.publicKeyJwk.crv).to.equal('X25519'); + }); }); describe('getDidDocument', async () => { @@ -40,12 +73,12 @@ describe('Web5DID', async () => { expect(resolved.didDocument).to.have.property('id', did); }); - it('should return null didDocument for an invalid DID', async () => { + it('should return undefined didDocument for an invalid DID', async () => { const did = 'did:key:invalid'; const resolved = await web5did.resolve(did); - expect(resolved.didDocument).to.be.null; + expect(resolved.didDocument).to.be.undefined; expect(resolved.didResolutionMetadata.error).to.equal('invalidDid'); }); }); diff --git a/tests/did/didUtils.spec.js b/tests/did/utils.spec.js similarity index 98% rename from tests/did/didUtils.spec.js rename to tests/did/utils.spec.js index 5397e833c..2a71b5c8b 100644 --- a/tests/did/didUtils.spec.js +++ b/tests/did/utils.spec.js @@ -1,12 +1,12 @@ import chaiAsPromised from 'chai-as-promised'; import chai, { expect } from 'chai'; -import * as DidUtils from '../../src/did/didUtils.js'; -import * as didDocuments from '../data/didDocuments.js'; +import * as DidUtils from '../../src/did/utils.js'; +import * as didDocuments from '../data/did-documents.js'; chai.use(chaiAsPromised); -describe('Web5DID', async () => { +describe('Web5Did', async () => { describe('DID Utils Tests', () => { diff --git a/tests/did/Web5DID.spec.js b/tests/did/web5-did.spec.js similarity index 89% rename from tests/did/Web5DID.spec.js rename to tests/did/web5-did.spec.js index cd3cd54a9..bd9652459 100644 --- a/tests/did/Web5DID.spec.js +++ b/tests/did/web5-did.spec.js @@ -3,15 +3,15 @@ import sinon from 'sinon'; import { Encoder } from '@tbd54566975/dwn-sdk-js'; import { base64UrlToString } from '../../src/utils.js'; -import { Web5 } from '../../src/Web5.js'; -import { Web5DID } from '../../src/did/Web5DID.js'; -import * as didDocuments from '../data/didDocuments.js'; +import { Web5 } from '../../src/web5.js'; +import { Web5Did } from '../../src/did/web5-did.js'; +import * as didDocuments from '../data/did-documents.js'; -describe('Web5DID', async () => { +describe('Web5Did', async () => { let web5did; beforeEach(function () { - web5did = new Web5DID(); + web5did = new Web5Did(); }); before(function () { @@ -42,7 +42,7 @@ describe('Web5DID', async () => { const decryptionResult = await web5.did.decrypt({ did: recipientDid.id, - privateKey: recipientDid.keys[0].keypair.privateKeyJwk.d, + privateKey: recipientDid.keys[0].keyPair.privateKeyJwk.d, payload: encryptionResult, }); @@ -146,39 +146,40 @@ describe('Web5DID', async () => { }); }); - describe('register', async () => { - it('should never expire registered DIDs', async function () { + + describe('manager', async () => { + it('should never expire managed DIDs', async function () { let resolved; const did = 'did:ion:abcd1234'; const didData = { connected: true, - did: did, endpoint: 'http://localhost:55500', }; - web5did.register(didData); + await web5did.manager.set(did, didData); resolved = await web5did.resolve(did); - expect(resolved.did).to.equal(did); + expect(resolved).to.not.be.undefined; + expect(resolved).to.equal(didData); this.clock.tick(2147483647); // Time travel 23.85 days resolved = await web5did.resolve(did); - expect(resolved.did).to.equal(did); + expect(resolved).to.not.be.undefined; + expect(resolved).to.equal(didData); }); it('should return object with keys undefined if key data not provided', async () => { const did = 'did:ion:abcd1234'; const didData = { connected: true, - did: did, endpoint: 'http://localhost:55500', }; - web5did.register(didData); + await web5did.manager.set(did, didData); const resolved = await web5did.resolve(did); expect(resolved.keys).to.be.undefined; - }); + }); }); }); \ No newline at end of file diff --git a/tests/storage/MemoryStorage.spec.js b/tests/storage/memory-storage.spec.js similarity index 92% rename from tests/storage/MemoryStorage.spec.js rename to tests/storage/memory-storage.spec.js index dda58f188..2c3fbf4b6 100644 --- a/tests/storage/MemoryStorage.spec.js +++ b/tests/storage/memory-storage.spec.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import { MemoryStorage } from '../../src/storage/MemoryStorage.js'; +import { MemoryStorage } from '../../src/storage/memory-storage.js'; describe('MemoryStorage', async () => { before(function () { @@ -57,14 +57,14 @@ describe('MemoryStorage', async () => { expect(valueInCache).to.equal('bValue'); }); - it('should delete specified entry', async () => { + it('should remove specified entry', async () => { const storage = new MemoryStorage(); await storage.set('key1', 'aValue'); await storage.set('key2', 'aValue'); await storage.set('key3', 'aValue'); - await storage.delete('key1'); + await storage.remove('key1'); let valueInCache = await storage.get('key1'); expect(valueInCache).to.be.undefined; @@ -72,7 +72,7 @@ describe('MemoryStorage', async () => { expect(valueInCache).to.equal('aValue'); }); - it('should delete all entries after `clear()`', async () => { + it('should remove all entries after `clear()`', async () => { const storage = new MemoryStorage(); await storage.set('key1', 'aValue'); From 706a87786edd72f5ae1b99742623b6517b0e0882 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Wed, 19 Apr 2023 16:54:22 -0400 Subject: [PATCH 02/19] Delete duplicate Storage.js Caused by git ignoring case --- src/storage/Storage.js | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 src/storage/Storage.js diff --git a/src/storage/Storage.js b/src/storage/Storage.js deleted file mode 100644 index 7548b581e..000000000 --- a/src/storage/Storage.js +++ /dev/null @@ -1,21 +0,0 @@ -class Storage { - async get(_key) { - throw 'subclass must override'; - } - - async set(_key, _value) { - throw 'subclass must override'; - } - - async remove(_key) { - throw 'subclass must override'; - } - - async clear() { - throw 'subclass must override'; - } -} - -export { - Storage, -}; From cbf7017aa7b0aa376352388c1c6e0bb41d6b0e7d Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Wed, 19 Apr 2023 16:54:45 -0400 Subject: [PATCH 03/19] Delete duplicate file Caused by git ignoring case --- src/Web5.js | 137 ---------------------------------------------------- 1 file changed, 137 deletions(-) delete mode 100644 src/Web5.js diff --git a/src/Web5.js b/src/Web5.js deleted file mode 100644 index a605f5f81..000000000 --- a/src/Web5.js +++ /dev/null @@ -1,137 +0,0 @@ -import { Web5Did } from './did/web5-did.js'; -import { Web5Dwn } from './dwn/web5-dwn.js'; -import { AppTransport } from './transport/app-transport.js'; -import { HttpTransport } from './transport/http-transport.js'; -import { isUnsignedMessage, parseUrl } from './utils.js'; - -export class Web5 extends EventTarget { - #dwn; - #did; - #transports; - - constructor(options = { }) { - super(); - - this.#dwn = new Web5Dwn(this, options?.dwn); - this.#did = new Web5Did(this); - this.#transports = { - app: new AppTransport(this), - http: new HttpTransport(this), - https: new HttpTransport(this), - }; - } - - get dwn() { - return this.#dwn; - } - - get did() { - return this.#did; - } - - get transports() { - return this.#transports; - } - - /** - * @param {string} target The DID to send the message to. - * @param {object} request - Object containing the request parameters. - * @param {string} request.author - The DID of the author of the message. - * @param {*} request.data - The message data (if any). - * @param {object} request.message - The DWeb message. - * @returns Promise - */ - async send(target, request) { - let { author, data, message } = request; - - if (isUnsignedMessage(message)) { - const resolvedAuthor = await this.#did.resolve(author); - - // If keys are not available to sign messages, transport the message to the specified agent. - if (!resolvedAuthor?.keys) { - if (resolvedAuthor?.connected) { - return this.#send([resolvedAuthor.endpoint], { author, data, message, target }); - } - - // TODO: Is this sufficient or might we improve how the calling app can respond by initiating a connect/re-connect flow? - return { status: { code: 401, detail: 'Local keys not available and remote agent not connected' } }; - } - - message = await this.#createSignedMessage(author, resolvedAuthor, message, data); - } - - const resolvedTarget = await this.#did.resolve(target); - - if (resolvedTarget?.connected) { - return this.#send([resolvedTarget.endpoint], { author, data, message, target }); - } else if (resolvedTarget?.didDocument) { - // Resolve the DWN endpoint(s) of the target and send using the endpoint's transport protocol (e.g., HTTP). - const dwnServices = await this.#did.getServices(target, { cache: true, type: 'DecentralizedWebNode' }); - const dwnNodes = dwnServices[0]?.serviceEndpoint?.nodes; - if (dwnNodes) { - return this.#send(dwnNodes, { author, data, message, target }); - } - return { status: { code: 400, detail: 'No DWN endpoints present in DID document. Request cannot be sent.' } }; - } - - return { status: { code: 400, detail: 'Target DID could not be resolved' } }; - } - - async #createSignedMessage(author, resolvedAuthor, message, data) { - const keyId = '#dwn'; - const authorizationSignatureInput = this.#dwn.sdk.Jws.createSignatureInput({ - keyId: author + keyId, - keyPair: resolvedAuthor.keys[keyId].keyPair, - }); - const signedMessage = await this.#dwn.sdk[message.interface + message.method].create({ - ...message, - authorizationSignatureInput, - data, - }); - delete signedMessage.data; - return signedMessage; - } - - /** - * Sends the message to one or more endpoint URIs - * - * If more than one endpoint is passed, each endpoint is tried serially until one succeeds or all fail. - * - * This strategy is used to account for cases like attempting to write large data streams to - * the DWN endpoints listed in a DID document. It would be inefficient to attempt to write data to - * multiple endpoints in parallel until the first one completes. Instead, we only try the next DWN if - * there is a failure. Additionally, per the DWN Specification, implementers SHOULD select from the - * Service Endpoint URIs in the nodes array in index order, so this function makes that approach easy. - * - * @param {string[]} endpoints - An array of one or more endpoints to send the message to. - * @param {object} request - Object containing the request parameters. - * @param {string} request.author - The DID of the author of the message. - * @param {*} request.data - The message data (if any). - * @param {object} request.message - The DWeb message. - * @param {string} request.target - The DID to send the message to. - * @returns {Promise} - */ - async #send(endpoints, request) { - let response, message = {}; - for (let endpoint of endpoints) { - try { - const url = parseUrl(endpoint); - response = await this.#transports[url?.protocol?.slice(0, -1)]?.send(url.href, request); - } catch (error) { - console.error(error); - // Intentionally ignore exception and try the next endpoint. - } - if (response) break; // Stop looping and return after the first endpoint successfully responds. - } - - if (!isUnsignedMessage(request.message)) { - // If the message is signed return the `descriptor`, and if present, `recordId`. - const { recordId = null, descriptor } = request.message.message; - message = { recordId, descriptor }; - } - - response ??= { status: { code: 503, detail: 'Service Unavailable' } }; - - return { message, ...response }; - } -} From 9f3e81207b0a26e1bde2ac96eb0ad9986a8dad4f Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Thu, 20 Apr 2023 06:20:42 -0400 Subject: [PATCH 04/19] Revert status code for 'No DWN endpoints present in DID document' back to 422 Co-authored-by: Frank Hinek Co-authored-by: Devin Rousso --- src/web5.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web5.js b/src/web5.js index a605f5f81..9ff0b78b0 100644 --- a/src/web5.js +++ b/src/web5.js @@ -71,7 +71,7 @@ export class Web5 extends EventTarget { if (dwnNodes) { return this.#send(dwnNodes, { author, data, message, target }); } - return { status: { code: 400, detail: 'No DWN endpoints present in DID document. Request cannot be sent.' } }; + return { status: { code: 422, detail: 'No DWN endpoints present in DID document. Request cannot be sent.' } }; } return { status: { code: 400, detail: 'Target DID could not be resolved' } }; From 61ca8b4fe958838906e5cd8b205e968ed91c1fab Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Thu, 20 Apr 2023 08:37:40 -0400 Subject: [PATCH 05/19] Bump dwn-sdk-js to 0.0.30 Signed-off-by: Frank Hinek --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index a5e021d25..4ce192e8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@decentralized-identity/ion-tools": "1.0.6", - "@tbd54566975/dwn-sdk-js": "0.0.30-unstable-2023-04-15-149c713", + "@tbd54566975/dwn-sdk-js": "0.0.30", "cross-fetch": "3.1.5", "ed2curve": "0.3.0", "readable-web-to-node-stream": "3.0.2", @@ -929,9 +929,9 @@ "integrity": "sha512-aWItSZvJj4+GI6FWkjZR13xPNPctq2RRakzo+O6vN7bC2yjwdg5EFpgaSAUn95b7BGSgcflvzVDPoKmJv24IOg==" }, "node_modules/@tbd54566975/dwn-sdk-js": { - "version": "0.0.30-unstable-2023-04-15-149c713", - "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.30-unstable-2023-04-15-149c713.tgz", - "integrity": "sha512-3xSbZ4DMvQUqCActdwuRRDfFVDjMHsX3tFxEn4u5O6SOmTdZ44AZQwxS8F0hINS5onnGjgfZNXc/NZ6FBmunDA==", + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.30.tgz", + "integrity": "sha512-PzO2r61G0dT3b32sBoR93ykBpkmLTDOc0vQAnqpiCRbX+Dx/3+yORbOFYLe4oKFYf/EnsohaiSXs9utcPpk6fQ==", "dependencies": { "@ipld/dag-cbor": "9.0.0", "@js-temporal/polyfill": "0.4.3", @@ -8007,9 +8007,9 @@ "integrity": "sha512-aWItSZvJj4+GI6FWkjZR13xPNPctq2RRakzo+O6vN7bC2yjwdg5EFpgaSAUn95b7BGSgcflvzVDPoKmJv24IOg==" }, "@tbd54566975/dwn-sdk-js": { - "version": "0.0.30-unstable-2023-04-15-149c713", - "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.30-unstable-2023-04-15-149c713.tgz", - "integrity": "sha512-3xSbZ4DMvQUqCActdwuRRDfFVDjMHsX3tFxEn4u5O6SOmTdZ44AZQwxS8F0hINS5onnGjgfZNXc/NZ6FBmunDA==", + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@tbd54566975/dwn-sdk-js/-/dwn-sdk-js-0.0.30.tgz", + "integrity": "sha512-PzO2r61G0dT3b32sBoR93ykBpkmLTDOc0vQAnqpiCRbX+Dx/3+yORbOFYLe4oKFYf/EnsohaiSXs9utcPpk6fQ==", "requires": { "@ipld/dag-cbor": "9.0.0", "@js-temporal/polyfill": "0.4.3", diff --git a/package.json b/package.json index 5bf3faf47..19168cda4 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ }, "dependencies": { "@decentralized-identity/ion-tools": "1.0.6", - "@tbd54566975/dwn-sdk-js": "0.0.30-unstable-2023-04-15-149c713", + "@tbd54566975/dwn-sdk-js": "0.0.30", "cross-fetch": "3.1.5", "ed2curve": "0.3.0", "readable-web-to-node-stream": "3.0.2", From a12165abd23a27468fabc252a2a08d4a579ed996 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Thu, 20 Apr 2023 13:06:06 -0400 Subject: [PATCH 06/19] Clean-up git case insensitivity issue --- src/transport/Transport.js | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/transport/Transport.js diff --git a/src/transport/Transport.js b/src/transport/Transport.js deleted file mode 100644 index 348a86c06..000000000 --- a/src/transport/Transport.js +++ /dev/null @@ -1,19 +0,0 @@ -class Transport { - #web5; - - constructor(web5) { - this.#web5 = web5; - } - - get web5() { - return this.#web5; - } - - async send(_endpoint, _request) { - throw 'subclass must override'; - } -} - -export { - Transport, -}; From a01471f36de09561c63155887265985b717fc000 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Thu, 20 Apr 2023 16:37:53 -0400 Subject: [PATCH 07/19] Make the Web5Did didStore private Signed-off-by: Frank Hinek --- src/did/web5-did.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/did/web5-did.js b/src/did/web5-did.js index 6f96b8fbe..54eda49d7 100644 --- a/src/did/web5-did.js +++ b/src/did/web5-did.js @@ -13,8 +13,8 @@ export class Web5Did { #didConnect; #web5; + #didStore = new MemoryStorage(); #resolvedDids = new MemoryStorage(); - didStore = new MemoryStorage(); constructor(web5) { this.#web5 = web5; @@ -40,11 +40,11 @@ export class Web5Did { get manager() { return { - clear: (...args) => DidManager.clear(...args, this.didStore), - exists: (...args) => DidManager.exists(...args, this.didStore), - get: (...args) => DidManager.get(...args, this.didStore), - remove: (...args) => DidManager.remove(...args, this.didStore), - set: (...args) => DidManager.set(...args, this.didStore), + clear: (...args) => DidManager.clear(...args, this.#didStore), + exists: (...args) => DidManager.exists(...args, this.#didStore), + get: (...args) => DidManager.get(...args, this.#didStore), + remove: (...args) => DidManager.remove(...args, this.#didStore), + set: (...args) => DidManager.set(...args, this.#didStore), }; } From fb2a003254ebd5dcb34815f0b1e9fcd33f107282 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Thu, 20 Apr 2023 16:50:00 -0400 Subject: [PATCH 08/19] Move Web5 response types to be adjacent to functions that return this type Signed-off-by: Frank Hinek --- src/types.js | 13 ------------- src/web5.js | 9 +++++++++ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/types.js b/src/types.js index b2e6a150b..879bc159b 100644 --- a/src/types.js +++ b/src/types.js @@ -2,19 +2,6 @@ * NOTE: These are temporary until Web5 JS can be converted to TypeScript and import the types from dwn-sdk-js. */ -/** - * Web5 JS type definitions. - */ - -/** - * @typedef {Object} Web5SendResponseMessage - * @property {ProtocolsConfigureDescriptor | ProtocolsQueryDescriptor | RecordsQueryDescriptor | RecordsReadDescriptor | RecordsWriteDescriptor} message - */ - -/** - * @typedef {MessageReplyOptions | Web5SendResponseMessage} Web5SendResponse - */ - /** * DWN SDK JS type definitions converted from TypeScript. */ diff --git a/src/web5.js b/src/web5.js index 9ff0b78b0..d851c045b 100644 --- a/src/web5.js +++ b/src/web5.js @@ -92,6 +92,15 @@ export class Web5 extends EventTarget { return signedMessage; } + /** + * @typedef {Object} Web5SendResponseMessage + * @property {ProtocolsConfigureDescriptor | ProtocolsQueryDescriptor | RecordsQueryDescriptor | RecordsReadDescriptor | RecordsWriteDescriptor} message + */ + + /** + * @typedef {MessageReplyOptions | Web5SendResponseMessage} Web5SendResponse + */ + /** * Sends the message to one or more endpoint URIs * From 7a40a6622e2eb9fc885d89b83934f373ded6689f Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Thu, 20 Apr 2023 16:51:46 -0400 Subject: [PATCH 09/19] Minor syntax changes to Record class Signed-off-by: Frank Hinek --- src/dwn/models/record.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/dwn/models/record.js b/src/dwn/models/record.js index 4395b0618..218c6f318 100644 --- a/src/dwn/models/record.js +++ b/src/dwn/models/record.js @@ -78,7 +78,7 @@ export class Record { // If neither of the above are true, then the record must be fetched from the DWN. this.#readableStream = this.#dwn.records.read(this.#target, { author: this.#author, message: { recordId: this.#recordId } }) .then((response) => response.record ) - .then((record) => { return record.data; }); + .then((record) => record.data); } if (typeof this.#encodedData === 'string') { @@ -107,7 +107,7 @@ export class Record { if (self.#readableStream) return self.#readableStream; return null; }, - then (...callbacks) { + then(...callbacks) { return this.stream().then(...callbacks); }, catch(callback) { @@ -239,6 +239,5 @@ export class Record { throw new Error(`${propertyName} is an immutable property. Its value cannot be changed.`); } }); - return true; } } From d08b75f64cfd5a8f21e0e613fecc4477e9bc78e8 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Thu, 20 Apr 2023 16:58:05 -0400 Subject: [PATCH 10/19] Change Record.create from getter to function Signed-off-by: Frank Hinek --- src/dwn/interfaces/records.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dwn/interfaces/records.js b/src/dwn/interfaces/records.js index 766619c23..d9ef8e3e2 100644 --- a/src/dwn/interfaces/records.js +++ b/src/dwn/interfaces/records.js @@ -9,8 +9,8 @@ export class Records extends Interface { super(dwn, 'Records'); } - get create() { - return this.write; + async create(...args) { + return this.write(...args); } async createFrom(target, request) { From d0a95c9f4d5b0c06bcaddea27921b750f9641701 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Thu, 20 Apr 2023 17:17:00 -0400 Subject: [PATCH 11/19] Change frozen/deleted terminology to be more descriptive/intuitive Signed-off-by: Frank Hinek --- src/dwn/models/record.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/dwn/models/record.js b/src/dwn/models/record.js index 218c6f318..716311c9b 100644 --- a/src/dwn/models/record.js +++ b/src/dwn/models/record.js @@ -13,7 +13,7 @@ export class Record { #target; #encodedData = null; - #isFrozen = false; + #isDeleted = false; #readableStream = null; constructor(dwn, options = { }) { @@ -43,7 +43,7 @@ export class Record { // Mutable Web5 Record Class properties. get author() { return this.#author; } - get isFrozen() { return this.#isFrozen; } + get isDeleted() { return this.#isDeleted; } get target() { return this.#target; } set author(author) { this.#author = author; } set target(target) { this.#target = target; } @@ -122,21 +122,21 @@ export class Record { */ async delete() { - if (this.isFrozen) throw new Error(`Error: Record with ID '${this.id}' was previously deleted.`); + if (this.isDeleted) throw new Error(`Error: Record with ID '${this.id}' was previously deleted.`); // Attempt to delete the record from the DWN. const response = await this.#dwn.records.delete(this.#target, { author: this.#author, message: { recordId: this.#recordId } }); if (response.status.code === 202) { - // If the record was successfully deleted, freeze the instance to prevent further modifications. - this.#freezeRecord(); + // If the record was successfully deleted, mark the instance as deleted to prevent further modifications. + this.#setDeletedStatus(true); } return response; } async update(options = { }) { - if (this.isFrozen) throw new Error(`Error: Record with ID '${this.id}' was previously deleted.`); + if (this.isDeleted) throw new Error(`Error: Record with ID '${this.id}' was previously deleted.`); // Begin assembling update message. let updateMessage = { ...this.#descriptor, ...options }; @@ -225,12 +225,8 @@ export class Record { }; } - #freezeRecord() { - this.#isFrozen = true; - } - - #unFreezeRecord() { - this.#isFrozen = false; + #setDeletedStatus(status) { + this.#isDeleted = status; } static #verifyPermittedMutation(propertiesToMutate, mutableDescriptorProperties) { From 1b4d64ec4f93fbcd7785ff2f99dafc6c7c7c6750 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Fri, 21 Apr 2023 05:45:26 -0400 Subject: [PATCH 12/19] Making named exports consistent throughout project Signed-off-by: Frank Hinek --- examples/simple-agent/src/utils.js | 21 ++++++--------------- src/did/methods/ion.js | 15 +++++---------- src/did/methods/key.js | 15 ++++----------- src/dwn/interfaces/permissions.js | 6 +----- src/dwn/interfaces/protocols.js | 6 +----- src/dwn/web5-dwn.js | 6 +----- src/storage/local-storage.js | 6 +----- src/storage/memory-storage.js | 6 +----- src/storage/storage.js | 6 +----- src/transport/transport.js | 6 +----- src/utils.js | 30 +++++++++--------------------- 11 files changed, 31 insertions(+), 92 deletions(-) diff --git a/examples/simple-agent/src/utils.js b/examples/simple-agent/src/utils.js index 2384f2fb2..0c20d6871 100644 --- a/examples/simple-agent/src/utils.js +++ b/examples/simple-agent/src/utils.js @@ -16,7 +16,7 @@ const messageStore = new MessageStoreLevel({ const dwnNode = await Dwn.create({ messageStore, dataStore }); -const web5 = new Web5({ dwn: { node: dwnNode }}); +export const web5 = new Web5({ dwn: { node: dwnNode }}); const etcPath = './etc'; const didStoragePath = `${etcPath}/did.json`; @@ -26,7 +26,7 @@ const require = createRequire(import.meta.url); const testProtocol = require('../resources/test-protocol.json'); const protocols = [testProtocol]; -async function getPort(processArgv) { +export async function getPort(processArgv) { const defaultPort = 8080; // Find the -p option and get the port number @@ -39,7 +39,7 @@ async function getPort(processArgv) { return port; } -async function loadConfig() { +export async function loadConfig() { if (!fs.existsSync(etcPath)) { // ensure that directory for persistent storage exists mkdirp.sync(etcPath); @@ -61,7 +61,7 @@ async function loadConfig() { }); } -async function initOperatorDid() { +export async function initOperatorDid() { const operatorDid = await web5.did.create('ion', { services: [ { @@ -76,7 +76,7 @@ async function initOperatorDid() { return operatorDid; } -async function initializeProtocols() { +export async function initializeProtocols() { for (let { protocol, definition } of protocols) { const queryResponse = await web5.dwn.protocols.query(didState.id, { author: didState.id, @@ -98,7 +98,7 @@ async function initializeProtocols() { } } -async function receiveHttp(ctx) { +export async function receiveHttp(ctx) { const encodedMessage = ctx.get(web5.transports.http.ENCODED_MESSAGE_HEADER); if (!encodedMessage) throw 'Message is missing or malformed'; @@ -116,12 +116,3 @@ async function receiveHttp(ctx) { message, }); } - -export { - getPort, - initializeProtocols, - initOperatorDid, - loadConfig, - receiveHttp, - web5, -}; diff --git a/src/did/methods/ion.js b/src/did/methods/ion.js index 42a0c74ea..4fa83c803 100644 --- a/src/did/methods/ion.js +++ b/src/did/methods/ion.js @@ -1,9 +1,11 @@ -import { DID, generateKeyPair, sign, verify } from '@decentralized-identity/ion-tools'; +import { DID, generateKeyPair } from '@decentralized-identity/ion-tools'; import { DidIonResolver } from '@tbd54566975/dwn-sdk-js'; +export { sign, verify } from '@decentralized-identity/ion-tools'; + const didIonResolver = new DidIonResolver(); -async function create(options = { }){ +export async function create(options = { }){ options.keys ||= [ { id: 'dwn', @@ -34,7 +36,7 @@ async function create(options = { }){ }; } -async function resolve(did) { +export async function resolve(did) { try { return await didIonResolver.resolve(did); } catch (error) { @@ -47,10 +49,3 @@ async function resolve(did) { }; } } - -export { - create, - sign, - verify, - resolve, -}; diff --git a/src/did/methods/key.js b/src/did/methods/key.js index bd6ae1649..906c6cde9 100644 --- a/src/did/methods/key.js +++ b/src/did/methods/key.js @@ -11,7 +11,7 @@ import { const didKeyResolver = new DidKeyResolver(); -async function create(options = { }) { +export async function create(options = { }) { // Generate new sign key pair. const verificationKeyPair = nacl.sign.keyPair(); const keyAgreementKeyPair = ed25519KeyPairToX25519(verificationKeyPair); @@ -46,7 +46,7 @@ async function create(options = { }) { }; } -async function resolve(did) { +export async function resolve(did) { return didKeyResolver.resolve(did); } @@ -62,7 +62,7 @@ async function resolve(did) { * @param {string} options.privateKeyJwk.x Base64url encoded public key * @returns {Uint8Array} Signature */ -async function sign(options) { +export async function sign(options) { const { data, privateKeyJwk } = options; const privateKeyBytes = Encoder.base64UrlToBytes(privateKeyJwk.d); @@ -91,7 +91,7 @@ async function sign(options) { * @param {string} options.publicKeyJwk.x Base64url encoded public key * @returns {boolean} */ -async function verify(options) { +export async function verify(options) { const { signature, data, publicKeyJwk } = options; const publicKeyBytes = Encoder.base64UrlToBytes(publicKeyJwk.x); @@ -109,10 +109,3 @@ async function verify(options) { return (result === null) ? false : true; } - -export { - create, - resolve, - sign, - verify, -}; \ No newline at end of file diff --git a/src/dwn/interfaces/permissions.js b/src/dwn/interfaces/permissions.js index ca2a9694c..65d3b215e 100644 --- a/src/dwn/interfaces/permissions.js +++ b/src/dwn/interfaces/permissions.js @@ -1,7 +1,7 @@ import { v4 as uuid } from 'uuid'; import { Interface } from './interface.js'; -class Permissions extends Interface { +export class Permissions extends Interface { constructor(dwn) { super(dwn, 'Permissions'); } @@ -18,7 +18,3 @@ class Permissions extends Interface { }); } } - -export { - Permissions, -}; diff --git a/src/dwn/interfaces/protocols.js b/src/dwn/interfaces/protocols.js index b08e2031e..8e5a5930b 100644 --- a/src/dwn/interfaces/protocols.js +++ b/src/dwn/interfaces/protocols.js @@ -1,6 +1,6 @@ import { Interface } from './interface.js'; -class Protocols extends Interface { +export class Protocols extends Interface { constructor(dwn) { super(dwn, 'Protocols'); } @@ -13,7 +13,3 @@ class Protocols extends Interface { return this.send('Query', target, request); } } - -export { - Protocols, -}; diff --git a/src/dwn/web5-dwn.js b/src/dwn/web5-dwn.js index faf38279a..606653766 100644 --- a/src/dwn/web5-dwn.js +++ b/src/dwn/web5-dwn.js @@ -7,7 +7,7 @@ import { createWeakSingletonAccessor } from '../utils.js'; const sharedNode = createWeakSingletonAccessor(() => Sdk.Dwn.create()); -class Web5Dwn { +export class Web5Dwn { #web5; #node; @@ -51,7 +51,3 @@ class Web5Dwn { return this.#node ??= sharedNode(); } } - -export { - Web5Dwn, -}; diff --git a/src/storage/local-storage.js b/src/storage/local-storage.js index 697525f22..1a86ba858 100644 --- a/src/storage/local-storage.js +++ b/src/storage/local-storage.js @@ -1,6 +1,6 @@ import { Storage } from './storage.js'; -class LocalStorage extends Storage { +export class LocalStorage extends Storage { async get(key) { return JSON.parse(localStorage.getItem(key)); } @@ -17,7 +17,3 @@ class LocalStorage extends Storage { localStorage.clear(); } } - -export { - LocalStorage, -}; diff --git a/src/storage/memory-storage.js b/src/storage/memory-storage.js index 2a9f2cc2c..f71851f20 100644 --- a/src/storage/memory-storage.js +++ b/src/storage/memory-storage.js @@ -1,6 +1,6 @@ import { Storage } from './storage.js'; -class MemoryStorage extends Storage { +export class MemoryStorage extends Storage { #dataForKey = new Map; #timeoutForKey = new Map; @@ -35,7 +35,3 @@ class MemoryStorage extends Storage { this.#timeoutForKey.clear(); } } - -export { - MemoryStorage, -}; diff --git a/src/storage/storage.js b/src/storage/storage.js index 7548b581e..604a59c2c 100644 --- a/src/storage/storage.js +++ b/src/storage/storage.js @@ -1,4 +1,4 @@ -class Storage { +export class Storage { async get(_key) { throw 'subclass must override'; } @@ -15,7 +15,3 @@ class Storage { throw 'subclass must override'; } } - -export { - Storage, -}; diff --git a/src/transport/transport.js b/src/transport/transport.js index 348a86c06..c6ac61151 100644 --- a/src/transport/transport.js +++ b/src/transport/transport.js @@ -1,4 +1,4 @@ -class Transport { +export class Transport { #web5; constructor(web5) { @@ -13,7 +13,3 @@ class Transport { throw 'subclass must override'; } } - -export { - Transport, -}; diff --git a/src/utils.js b/src/utils.js index 598882f0b..c15314258 100644 --- a/src/utils.js +++ b/src/utils.js @@ -14,7 +14,7 @@ export function bytesToObject(bytes) { return JSON.parse(objectString); } -function createWeakSingletonAccessor(creator) { +export function createWeakSingletonAccessor(creator) { let weakref = null; return function() { let object = weakref?.deref(); @@ -26,14 +26,14 @@ function createWeakSingletonAccessor(creator) { }; } -function isEmptyObject(obj) { +export function isEmptyObject(obj) { if (typeof obj === 'object' && obj !== null) { return Object.keys(obj).length === 0; } return false; } -function parseJson(str) { +export function parseJson(str) { try { return JSON.parse(str); } catch { @@ -41,7 +41,7 @@ function parseJson(str) { } } -function parseUrl(str) { +export function parseUrl(str) { try { return new URL(str); } catch { @@ -49,7 +49,7 @@ function parseUrl(str) { } } -function pascalToKebabCase(str) { +export function pascalToKebabCase(str) { return str .replace(/([a-z0-9])([A-Z])/g, '$1-$2') .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') @@ -59,7 +59,7 @@ function pascalToKebabCase(str) { /** * Set/detect the media type and return the data as bytes. */ -const dataToBytes = (data, dataFormat) => { +export const dataToBytes = (data, dataFormat) => { let dataBytes = data; // Check for Object or String, and if neither, assume bytes. @@ -88,15 +88,15 @@ const dataToBytes = (data, dataFormat) => { * @param {{}} message * @returns boolean */ -function isUnsignedMessage(message) { +export function isUnsignedMessage(message) { return message?.message?.authorization ? false : true; } -function objectValuesBytesToBase64Url(obj) { +export function objectValuesBytesToBase64Url(obj) { return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, Encoder.bytesToBase64Url(value)])); } -function objectValuesBase64UrlToBytes(obj) { +export function objectValuesBase64UrlToBytes(obj) { return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, Encoder.base64UrlToBytes(value)])); } @@ -109,15 +109,3 @@ function objectValuesBase64UrlToBytes(obj) { const toType = (obj) => { return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); }; - -export { - createWeakSingletonAccessor, - dataToBytes, - isEmptyObject, - isUnsignedMessage, - objectValuesBase64UrlToBytes, - objectValuesBytesToBase64Url, - parseJson, - parseUrl, - pascalToKebabCase, -}; From 6d63a0ba443e120f9ffcc0be5020601dbea4f67f Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Fri, 21 Apr 2023 06:11:27 -0400 Subject: [PATCH 13/19] Update simple-agent example to define location for EventLog Signed-off-by: Frank Hinek --- examples/simple-agent/.gitignore | 1 + examples/simple-agent/src/utils.js | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/simple-agent/.gitignore b/examples/simple-agent/.gitignore index cf58023e7..9cda4e2f9 100644 --- a/examples/simple-agent/.gitignore +++ b/examples/simple-agent/.gitignore @@ -1,3 +1,4 @@ DATASTORE-* +EVENTLOG-* INDEX-* MESSAGESTORE-* \ No newline at end of file diff --git a/examples/simple-agent/src/utils.js b/examples/simple-agent/src/utils.js index 0c20d6871..d2782cfc5 100644 --- a/examples/simple-agent/src/utils.js +++ b/examples/simple-agent/src/utils.js @@ -1,5 +1,5 @@ import getRawBody from 'raw-body'; -import { DataStoreLevel, Dwn, MessageStoreLevel } from '@tbd54566975/dwn-sdk-js'; +import { DataStoreLevel, Dwn, EventLogLevel, MessageStoreLevel } from '@tbd54566975/dwn-sdk-js'; import { Web5 } from '@tbd54566975/web5'; import fs from 'node:fs'; import mkdirp from 'mkdirp'; @@ -9,12 +9,13 @@ import { createRequire } from 'node:module'; // in the same directory. If you don't do this, you will get LevelDB lock errors. const port = await getPort(process.argv); const dataStore = new DataStoreLevel({ blockstoreLocation: `DATASTORE-${port}` }); +const eventLog = new EventLogLevel({ location: `EVENTLOG-${port}` }); const messageStore = new MessageStoreLevel({ blockstoreLocation : `MESSAGESTORE-${port}`, indexLocation : `INDEX-${port}`, }); -const dwnNode = await Dwn.create({ messageStore, dataStore }); +const dwnNode = await Dwn.create({ dataStore, eventLog, messageStore }); export const web5 = new Web5({ dwn: { node: dwnNode }}); @@ -57,7 +58,7 @@ export async function loadConfig() { web5.did.manager.set(didState.id, { connected: true, endpoint: 'app://dwn', - keys: didState.keys['#dwn'].keyPair, + keys: { ['#dwn']: { keyPair: didState.keys[0].keyPair} }, }); } From 3f4b8d4a885e2fc59d502621e1a0ac85056536f6 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Fri, 21 Apr 2023 06:30:59 -0400 Subject: [PATCH 14/19] Refactor DID Manager Signed-off-by: Frank Hinek --- src/did/manager.js | 44 +++++++++++++++++++++++++------------------- src/did/web5-did.js | 16 +++++++++------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/did/manager.js b/src/did/manager.js index 7903f4e6b..1fea1b2f1 100644 --- a/src/did/manager.js +++ b/src/did/manager.js @@ -1,22 +1,28 @@ -export async function clear(store) { - if (store){ - store.clear(); - } -} - -export async function exists(id, store) { - const value = await store.get(id); - return value !== undefined; -} +export class DidManager { + #store; -export async function get(id, store) { - return store.get(id); -} + constructor(options) { + this.#store = options.store; + } -export async function remove(id, store) { - store.remove(id); + async clear() { + this.#store.clear(); + } + + async exists(id) { + const value = await this.#store.get(id); + return value !== undefined; + } + + async get(id) { + return this.#store.get(id); + } + + async remove(id) { + this.#store.remove(id); + } + + async set(id, value) { + this.#store.set(id, value); + } } - -export async function set(id, value, store) { - store.set(id, value); -} \ No newline at end of file diff --git a/src/did/web5-did.js b/src/did/web5-did.js index 54eda49d7..47faae540 100644 --- a/src/did/web5-did.js +++ b/src/did/web5-did.js @@ -2,7 +2,7 @@ import { Encoder } from '@tbd54566975/dwn-sdk-js'; import { DidConnect } from './connect/connect.js'; import * as CryptoCiphers from './crypto/ciphers.js'; -import * as DidManager from './manager.js'; +import { DidManager } from './manager.js'; import * as Methods from './methods/methods.js'; import * as DidUtils from './utils.js'; import { MemoryStorage } from '../storage/memory-storage.js'; @@ -13,7 +13,7 @@ export class Web5Did { #didConnect; #web5; - #didStore = new MemoryStorage(); + #didManager; #resolvedDids = new MemoryStorage(); constructor(web5) { @@ -28,6 +28,8 @@ export class Web5Did { // Bind functions to the instance of DidConnect this.#didConnect.connect = this.#didConnect.connect.bind(this.#didConnect); this.#didConnect.permissionsRequest = this.#didConnect.permissionsRequest.bind(this.#didConnect); + + this.#didManager = new DidManager({ store: new MemoryStorage() }); } get connect() { @@ -40,11 +42,11 @@ export class Web5Did { get manager() { return { - clear: (...args) => DidManager.clear(...args, this.#didStore), - exists: (...args) => DidManager.exists(...args, this.#didStore), - get: (...args) => DidManager.get(...args, this.#didStore), - remove: (...args) => DidManager.remove(...args, this.#didStore), - set: (...args) => DidManager.set(...args, this.#didStore), + clear: () => this.#didManager.clear(), + exists: (...args) => this.#didManager.exists(...args), + get: (...args) => this.#didManager.get(...args), + remove: (...args) => this.#didManager.remove(...args), + set: (...args) => this.#didManager.set(...args), }; } From 9786d34e0dedaeb68c519b78844cd613042f819b Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Fri, 21 Apr 2023 07:11:55 -0400 Subject: [PATCH 15/19] Address minor nits Signed-off-by: Frank Hinek --- examples/simple-agent/package.json | 2 +- src/dwn/models/record.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/simple-agent/package.json b/examples/simple-agent/package.json index 7f4055f1e..581a17e32 100644 --- a/examples/simple-agent/package.json +++ b/examples/simple-agent/package.json @@ -16,7 +16,7 @@ "dependencies": { "@koa/cors": "4.0.0", "@koa/router": "12.0.0", - "@tbd54566975/web5": "0.3.1-unstable.e2dfe57-2023.3.23-19-07-37", + "@tbd54566975/web5": "0.5.0", "koa": "2.14.1", "koa-body": "6.0.1", "mkdirp": "2.1.5", diff --git a/src/dwn/models/record.js b/src/dwn/models/record.js index 716311c9b..6ff2471b6 100644 --- a/src/dwn/models/record.js +++ b/src/dwn/models/record.js @@ -36,7 +36,6 @@ export class Record { // If the record was created from a RecordsRead reply then it will have a `data` property. if (options?.data) { - // this.#readableStream = isReadableWebStream(options.data) ? toIsomorphicReadableStream(options.data) : options.data; this.#readableStream = isReadableWebStream(options.data) ? new ReadableWebToNodeStream(options.data) : options.data; } } From 6a95eca9d7dc4f6fe2a6c186ec8d0f85d9fb1f01 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Fri, 21 Apr 2023 08:12:50 -0400 Subject: [PATCH 16/19] Revert change from storage classes Signed-off-by: Frank Hinek --- src/did/manager.js | 4 ++-- src/did/web5-did.js | 2 +- src/storage/local-storage.js | 2 +- src/storage/memory-storage.js | 4 ++-- src/storage/storage.js | 2 +- tests/storage/memory-storage.spec.js | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/did/manager.js b/src/did/manager.js index 1fea1b2f1..71aa3167a 100644 --- a/src/did/manager.js +++ b/src/did/manager.js @@ -18,8 +18,8 @@ export class DidManager { return this.#store.get(id); } - async remove(id) { - this.#store.remove(id); + async delete(id) { + this.#store.delete(id); } async set(id, value) { diff --git a/src/did/web5-did.js b/src/did/web5-did.js index 47faae540..ab01f044f 100644 --- a/src/did/web5-did.js +++ b/src/did/web5-did.js @@ -45,7 +45,7 @@ export class Web5Did { clear: () => this.#didManager.clear(), exists: (...args) => this.#didManager.exists(...args), get: (...args) => this.#didManager.get(...args), - remove: (...args) => this.#didManager.remove(...args), + delete: (...args) => this.#didManager.delete(...args), set: (...args) => this.#didManager.set(...args), }; } diff --git a/src/storage/local-storage.js b/src/storage/local-storage.js index 1a86ba858..84ef90262 100644 --- a/src/storage/local-storage.js +++ b/src/storage/local-storage.js @@ -9,7 +9,7 @@ export class LocalStorage extends Storage { localStorage.setItem(key, JSON.stringify(value)); } - async remove(key) { + async delete(key) { localStorage.removeItem(key); } diff --git a/src/storage/memory-storage.js b/src/storage/memory-storage.js index f71851f20..3b15808f2 100644 --- a/src/storage/memory-storage.js +++ b/src/storage/memory-storage.js @@ -13,13 +13,13 @@ export class MemoryStorage extends Storage { if (Number.isFinite(options?.timeout)) { const timeout = setTimeout(() => { - this.remove(key); + this.delete(key); }, options.timeout); this.#timeoutForKey.set(key, timeout); } } - async remove(key) { + async delete(key) { this.#dataForKey.delete(key); clearTimeout(this.#timeoutForKey.get(key)); diff --git a/src/storage/storage.js b/src/storage/storage.js index 604a59c2c..0862faed8 100644 --- a/src/storage/storage.js +++ b/src/storage/storage.js @@ -7,7 +7,7 @@ export class Storage { throw 'subclass must override'; } - async remove(_key) { + async delete(_key) { throw 'subclass must override'; } diff --git a/tests/storage/memory-storage.spec.js b/tests/storage/memory-storage.spec.js index 2c3fbf4b6..9d41edbc0 100644 --- a/tests/storage/memory-storage.spec.js +++ b/tests/storage/memory-storage.spec.js @@ -57,14 +57,14 @@ describe('MemoryStorage', async () => { expect(valueInCache).to.equal('bValue'); }); - it('should remove specified entry', async () => { + it('should delete specified entry', async () => { const storage = new MemoryStorage(); await storage.set('key1', 'aValue'); await storage.set('key2', 'aValue'); await storage.set('key3', 'aValue'); - await storage.remove('key1'); + await storage.delete('key1'); let valueInCache = await storage.get('key1'); expect(valueInCache).to.be.undefined; @@ -72,7 +72,7 @@ describe('MemoryStorage', async () => { expect(valueInCache).to.equal('aValue'); }); - it('should remove all entries after `clear()`', async () => { + it('should delete all entries after `clear()`', async () => { const storage = new MemoryStorage(); await storage.set('key1', 'aValue'); From 9870e2fc5d088c1ad216eeecefd0d81b23d0e8f8 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Fri, 21 Apr 2023 10:11:20 -0400 Subject: [PATCH 17/19] Collection of small enhancements Signed-off-by: Frank Hinek --- examples/test-dashboard/desktop-agent.html | 2 +- src/dwn/interfaces/records.js | 25 +++++++++++----------- src/dwn/models/record.js | 16 +++++++------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/examples/test-dashboard/desktop-agent.html b/examples/test-dashboard/desktop-agent.html index f09f5ab5b..ce4aed003 100644 --- a/examples/test-dashboard/desktop-agent.html +++ b/examples/test-dashboard/desktop-agent.html @@ -716,7 +716,7 @@ create_from_record_button.addEventListener('execute', async event => { const { record: sourceRecord } = await web5.dwn.records.create(myDid, { author: myDid, - data: 'Hello, world!', + data: 'Source record to create from', message: { published: true, schema: 'foo/text', diff --git a/src/dwn/interfaces/records.js b/src/dwn/interfaces/records.js index d9ef8e3e2..95c70ea2f 100644 --- a/src/dwn/interfaces/records.js +++ b/src/dwn/interfaces/records.js @@ -2,7 +2,7 @@ import { DwnConstant } from '@tbd54566975/dwn-sdk-js'; import { Interface } from './interface.js'; import { Record } from '../models/record.js'; -import { dataToBytes } from '../../utils.js'; +import { dataToBytes, isEmptyObject } from '../../utils.js'; export class Records extends Interface { constructor(dwn) { @@ -14,31 +14,34 @@ export class Records extends Interface { } async createFrom(target, request) { - const { author: inheritedAuthor, target: _, ...inheritedProperties } = request.record.toJSON(); + const { author: inheritedAuthor, ...inheritedProperties } = request.record.toJSON(); + + // Remove target from inherited properties since target is being explicitly defined in method parameters. + delete inheritedProperties.target; + // If `data` is being updated then `dataCid` and `dataSize` must not be present. - if (request?.data !== undefined) { + if (request.data !== undefined) { delete inheritedProperties.dataCid; delete inheritedProperties.dataSize; } // If `published` is set to false, ensure that `datePublished` is undefined. Otherwise, DWN SDK's schema validation // will throw an error if `published` is false but `datePublished` is set. - if (request?.message?.published === false && inheritedProperties?.datePublished !== undefined) { + if (request.message?.published === false && inheritedProperties.datePublished !== undefined) { delete inheritedProperties.datePublished; delete inheritedProperties.published; } // If the request changes the `author` or message `descriptor` then the deterministic `recordId` will change. // As a result, we will discard the `recordId` if either of these changes occur. - if ((request?.message && Object.keys(request.message).length > 0) - || (request?.author && request.author !== inheritedAuthor)) { + if (!isEmptyObject(request.message) || (request.author && request.author !== inheritedAuthor)) { delete inheritedProperties.recordId; } return this.write(target, { - author: request?.author || inheritedAuthor, - data: request?.data, + author: request.author || inheritedAuthor, + data: request.data, message: { ...inheritedProperties, ...request.message, @@ -64,11 +67,8 @@ export class Records extends Interface { async query(target, request) { const response = await this.send('Query', target, request); + const entries = response.entries.map(entry => new Record(this.dwn, { ...entry, target, author: request.author })); - const entries = []; - response.entries.forEach(entry => { - entries.push(new Record(this.dwn, { ...entry, target, author: request.author })); - }); return { ...response, entries }; } @@ -98,6 +98,7 @@ export class Records extends Interface { record = new Record(this.dwn, { ...response.message, encodedData, target, author: request.author }); } + return { ...response, record }; } } diff --git a/src/dwn/models/record.js b/src/dwn/models/record.js index 6ff2471b6..b75ebb4c1 100644 --- a/src/dwn/models/record.js +++ b/src/dwn/models/record.js @@ -22,8 +22,8 @@ export class Record { // RecordsWriteMessage properties. const { author, contextId = undefined, descriptor, recordId = null, target } = options; this.#contextId = contextId; - if (descriptor?.data) delete descriptor.data; this.#descriptor = descriptor ?? { }; + delete this.#descriptor.data; this.#recordId = recordId; // Store the target and author DIDs that were used to create the message to use for subsequent reads, etc. @@ -143,7 +143,7 @@ export class Record { // If `data` is being updated then `dataCid` and `dataSize` must be undefined and the `data` property is passed as // a top-level property to `web5.dwn.records.write()`. let data; - if (options?.data !== undefined) { + if (options.data !== undefined) { delete updateMessage.dataCid; delete updateMessage.dataSize; data = options.data; @@ -157,13 +157,13 @@ export class Record { // If a new `dateModified` was not provided, remove it from the updateMessage to let the DWN SDK auto-fill. // This is necessary because otherwise DWN SDK throws an Error 409 Conflict due to attempting to overwrite a record // when the `dateModified` timestamps are identical. - if (options?.dateModified === undefined) { + if (options.dateModified === undefined) { delete updateMessage.dateModified; } // If `published` is set to false, ensure that `datePublished` is undefined. Otherwise, DWN SDK's schema validation // will throw an error if `published` is false but `datePublished` is set. - if (options?.published === false && updateMessage?.datePublished !== undefined) { + if (options.published === false && updateMessage.datePublished !== undefined) { delete updateMessage.datePublished; } @@ -229,10 +229,10 @@ export class Record { } static #verifyPermittedMutation(propertiesToMutate, mutableDescriptorProperties) { - propertiesToMutate.forEach(propertyName => { - if (!mutableDescriptorProperties.includes(propertyName)) { - throw new Error(`${propertyName} is an immutable property. Its value cannot be changed.`); + for (const property of propertiesToMutate) { + if (!mutableDescriptorProperties.includes(property)) { + throw new Error(`${property} is an immutable property. Its value cannot be changed.`); } - }); + } } } From 78d637d1e0646c06fae6ec3b0fd7dbac930bd995 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Fri, 21 Apr 2023 11:29:44 -0400 Subject: [PATCH 18/19] Improve defensiveness and readability of send functions for Web5 and HTTP transport Signed-off-by: Frank Hinek --- src/transport/http-transport.js | 2 +- src/web5.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/transport/http-transport.js b/src/transport/http-transport.js index 3652f797a..7c3110b8d 100644 --- a/src/transport/http-transport.js +++ b/src/transport/http-transport.js @@ -52,7 +52,7 @@ export class HttpTransport extends Transport { if (web5ResponseHeader) { // RecordsRead responses return `message` and `status` as header values, with a `data` ReadableStream in the body. const { entries = null, message, record, status } = await this.decodeMessage(web5ResponseHeader); - return { entries, message, record: { data: response.body, ...record }, status }; + return { entries, message, record: { ...record, data: response.body }, status }; } else { // All other DWN responses return `entries`, `message`, and `status` as stringified JSON in the body. diff --git a/src/web5.js b/src/web5.js index d851c045b..767c6e468 100644 --- a/src/web5.js +++ b/src/web5.js @@ -121,7 +121,7 @@ export class Web5 extends EventTarget { * @returns {Promise} */ async #send(endpoints, request) { - let response, message = {}; + let response; for (let endpoint of endpoints) { try { const url = parseUrl(endpoint); @@ -133,14 +133,14 @@ export class Web5 extends EventTarget { if (response) break; // Stop looping and return after the first endpoint successfully responds. } - if (!isUnsignedMessage(request.message)) { + if (response && !isUnsignedMessage(request.message)) { // If the message is signed return the `descriptor`, and if present, `recordId`. const { recordId = null, descriptor } = request.message.message; - message = { recordId, descriptor }; + response.message = { recordId, descriptor }; } response ??= { status: { code: 503, detail: 'Service Unavailable' } }; - return { message, ...response }; + return response; } } From 583bd3fe806e21a20bcf347c32f075f19232657b Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Fri, 21 Apr 2023 11:40:05 -0400 Subject: [PATCH 19/19] Bump ION tools Signed-off-by: Frank Hinek --- package-lock.json | 101 ++++++++++++++++------------------------------ package.json | 2 +- 2 files changed, 36 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4ce192e8a..7f3b59181 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.6.0", "license": "Apache-2.0", "dependencies": { - "@decentralized-identity/ion-tools": "1.0.6", + "@decentralized-identity/ion-tools": "1.0.7", "@tbd54566975/dwn-sdk-js": "0.0.30", "cross-fetch": "3.1.5", "ed2curve": "0.3.0", @@ -51,6 +51,16 @@ "node": ">=0.1.90" } }, + "node_modules/@decentralized-identity/ion-pow-sdk": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@decentralized-identity/ion-pow-sdk/-/ion-pow-sdk-1.0.17.tgz", + "integrity": "sha512-vk7DTDM8aKDbFyu1ad/qkoRrGL4q+KvNeL/FNZXhkWPaDhVExBN/qGEoRLf1YSfFe+myto3+4RYTPut+riiqnw==", + "dependencies": { + "buffer": "6.0.3", + "cross-fetch": "3.1.5", + "hash-wasm": "4.9.0" + } + }, "node_modules/@decentralized-identity/ion-sdk": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@decentralized-identity/ion-sdk/-/ion-sdk-0.6.0.tgz", @@ -92,15 +102,15 @@ "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==" }, "node_modules/@decentralized-identity/ion-tools": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@decentralized-identity/ion-tools/-/ion-tools-1.0.6.tgz", - "integrity": "sha512-xi1QudddgxUBzkN0cyerIZxxrZvBCO1RB3xf/eEbh0ZeSZe/WMgAEPjS3ehfUwAsDMKnf2REC3CPk5SphBA9OA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@decentralized-identity/ion-tools/-/ion-tools-1.0.7.tgz", + "integrity": "sha512-Rhz1pCBG3teq9PdeG54JC5KSwhgwkvY4FWufpsRpabrb2i5nPr804i4KfEwXzIdQxxJmzUVeS3+ypkrccl4WVQ==", "dependencies": { + "@decentralized-identity/ion-pow-sdk": "1.0.17", "@decentralized-identity/ion-sdk": "0.6.0", "@noble/ed25519": "1.6.0", "@noble/secp256k1": "1.5.5", "cross-fetch": "3.1.5", - "ion-pow-sdk": "1.0.16", "multiformats": "9.6.4" }, "engines": { @@ -3590,9 +3600,9 @@ ] }, "node_modules/hash-wasm": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.4.1.tgz", - "integrity": "sha512-6hbcJY74kQQOAoDQZgm0rku51jlf0f3+g+q+1CI3vNvabF27SQuVJm86FnMNMaw8WEjBeW4U5GWX1KsKd8qZCw==" + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.9.0.tgz", + "integrity": "sha512-7SW7ejyfnRxuOc7ptQHSf4LDoZaWOivfzqw+5rpcQku0nHfmicPKE51ra9BiRLAmT8+gGLestr1XroUkqdjL6w==" }, "node_modules/hash.js": { "version": "1.1.7", @@ -3766,32 +3776,6 @@ "resolved": "https://registry.npmjs.org/interface-store/-/interface-store-2.0.2.tgz", "integrity": "sha512-rScRlhDcz6k199EkHqT8NpM87ebN89ICOzILoBHgaG36/WX50N32BnU/kpZgCGPLhARRAWUUX5/cyaIjt7Kipg==" }, - "node_modules/ion-pow-sdk": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/ion-pow-sdk/-/ion-pow-sdk-1.0.16.tgz", - "integrity": "sha512-J5rwzf3Ntyv+mEUs+Mpor78E83hYsq8CDSf3SkxJXAajb4NYa3+medR7SoolpkMjEaMd2KeZg/+2w2XdrIrtwA==", - "dependencies": { - "buffer": "6.0.3", - "cross-fetch": "3.1.2", - "hash-wasm": "4.4.1" - } - }, - "node_modules/ion-pow-sdk/node_modules/cross-fetch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.2.tgz", - "integrity": "sha512-+JhD65rDNqLbGmB3Gzs3HrEKC0aQnD+XA3SY6RjgkF88jV2q5cTc5+CwxlS3sdmLk98gpPt5CF9XRnPdlxZe6w==", - "dependencies": { - "node-fetch": "2.6.1" - } - }, - "node_modules/ion-pow-sdk/node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "engines": { - "node": "4.x || >=6.0.0" - } - }, "node_modules/ipfs-unixfs": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/ipfs-unixfs/-/ipfs-unixfs-6.0.9.tgz", @@ -7417,6 +7401,16 @@ "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true }, + "@decentralized-identity/ion-pow-sdk": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@decentralized-identity/ion-pow-sdk/-/ion-pow-sdk-1.0.17.tgz", + "integrity": "sha512-vk7DTDM8aKDbFyu1ad/qkoRrGL4q+KvNeL/FNZXhkWPaDhVExBN/qGEoRLf1YSfFe+myto3+4RYTPut+riiqnw==", + "requires": { + "buffer": "6.0.3", + "cross-fetch": "3.1.5", + "hash-wasm": "4.9.0" + } + }, "@decentralized-identity/ion-sdk": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@decentralized-identity/ion-sdk/-/ion-sdk-0.6.0.tgz", @@ -7448,15 +7442,15 @@ } }, "@decentralized-identity/ion-tools": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@decentralized-identity/ion-tools/-/ion-tools-1.0.6.tgz", - "integrity": "sha512-xi1QudddgxUBzkN0cyerIZxxrZvBCO1RB3xf/eEbh0ZeSZe/WMgAEPjS3ehfUwAsDMKnf2REC3CPk5SphBA9OA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@decentralized-identity/ion-tools/-/ion-tools-1.0.7.tgz", + "integrity": "sha512-Rhz1pCBG3teq9PdeG54JC5KSwhgwkvY4FWufpsRpabrb2i5nPr804i4KfEwXzIdQxxJmzUVeS3+ypkrccl4WVQ==", "requires": { + "@decentralized-identity/ion-pow-sdk": "1.0.17", "@decentralized-identity/ion-sdk": "0.6.0", "@noble/ed25519": "1.6.0", "@noble/secp256k1": "1.5.5", "cross-fetch": "3.1.5", - "ion-pow-sdk": "1.0.16", "multiformats": "9.6.4" }, "dependencies": { @@ -10132,9 +10126,9 @@ } }, "hash-wasm": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.4.1.tgz", - "integrity": "sha512-6hbcJY74kQQOAoDQZgm0rku51jlf0f3+g+q+1CI3vNvabF27SQuVJm86FnMNMaw8WEjBeW4U5GWX1KsKd8qZCw==" + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.9.0.tgz", + "integrity": "sha512-7SW7ejyfnRxuOc7ptQHSf4LDoZaWOivfzqw+5rpcQku0nHfmicPKE51ra9BiRLAmT8+gGLestr1XroUkqdjL6w==" }, "hash.js": { "version": "1.1.7", @@ -10271,31 +10265,6 @@ "resolved": "https://registry.npmjs.org/interface-store/-/interface-store-2.0.2.tgz", "integrity": "sha512-rScRlhDcz6k199EkHqT8NpM87ebN89ICOzILoBHgaG36/WX50N32BnU/kpZgCGPLhARRAWUUX5/cyaIjt7Kipg==" }, - "ion-pow-sdk": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/ion-pow-sdk/-/ion-pow-sdk-1.0.16.tgz", - "integrity": "sha512-J5rwzf3Ntyv+mEUs+Mpor78E83hYsq8CDSf3SkxJXAajb4NYa3+medR7SoolpkMjEaMd2KeZg/+2w2XdrIrtwA==", - "requires": { - "buffer": "6.0.3", - "cross-fetch": "3.1.2", - "hash-wasm": "4.4.1" - }, - "dependencies": { - "cross-fetch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.2.tgz", - "integrity": "sha512-+JhD65rDNqLbGmB3Gzs3HrEKC0aQnD+XA3SY6RjgkF88jV2q5cTc5+CwxlS3sdmLk98gpPt5CF9XRnPdlxZe6w==", - "requires": { - "node-fetch": "2.6.1" - } - }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" - } - } - }, "ipfs-unixfs": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/ipfs-unixfs/-/ipfs-unixfs-6.0.9.tgz", diff --git a/package.json b/package.json index 19168cda4..8a230fa1c 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "source-map-loader": "4.0.1" }, "dependencies": { - "@decentralized-identity/ion-tools": "1.0.6", + "@decentralized-identity/ion-tools": "1.0.7", "@tbd54566975/dwn-sdk-js": "0.0.30", "cross-fetch": "3.1.5", "ed2curve": "0.3.0",