diff --git a/.changeset/few-oranges-compare.md b/.changeset/few-oranges-compare.md new file mode 100644 index 000000000..9c8040ac8 --- /dev/null +++ b/.changeset/few-oranges-compare.md @@ -0,0 +1,12 @@ +--- +"@web5/identity-agent": patch +"@web5/proxy-agent": patch +"@web5/user-agent": patch +"@web5/agent": patch +"@web5/api": patch +--- + +Upgrade DWN SDK with newest features + +- remove `Permissions` interface and replace permissions with a first-class protocol representing it +- adding `RecordsTags` functionality diff --git a/CODEOWNERS b/CODEOWNERS index 1382d9388..5e71ff8b1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -18,9 +18,9 @@ # These are owners of any file in the `agent`, `user-agent`, `proxy-agent`, `identity-agent`, and # `api` packages and their sub-directories. -/packages/agent @lirancohen @frankhinek @csuwildcat @mistermoe -/packages/proxy-agent @lirancohen @frankhinek @csuwildcat @mistermoe -/packages/user-agent @lirancohen @frankhinek @csuwildcat @mistermoe -/packages/identity-agent @lirancohen @frankhinek @csuwildcat @mistermoe -/packages/api @lirancohen @frankhinek @csuwildcat @mistermoe - \ No newline at end of file +/packages/agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim +/packages/proxy-agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim +/packages/user-agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim +/packages/identity-agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim +/packages/api @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim + diff --git a/package.json b/package.json index 31135457a..9086084e0 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@changesets/cli": "^2.27.1", "@npmcli/package-json": "5.0.0", "@typescript-eslint/eslint-plugin": "6.4.0", - "@web5/dwn-server": "0.1.17", + "@web5/dwn-server": "0.2.1", "eslint-plugin-mocha": "10.1.0", "npkill": "0.11.3" }, diff --git a/packages/agent/package.json b/packages/agent/package.json index b4de4b539..1cf7765ca 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -71,10 +71,10 @@ "dependencies": { "@noble/ciphers": "0.4.1", "@scure/bip39": "1.2.2", - "@tbd54566975/dwn-sdk-js": "0.2.22", + "@tbd54566975/dwn-sdk-js": "0.3.1", "@web5/common": "1.0.0", "@web5/crypto": "1.0.0", - "@web5/dids": "1.0.0", + "@web5/dids": "1.0.1", "abstract-level": "1.0.4", "ed25519-keygen": "0.4.11", "level": "8.0.0", diff --git a/packages/api/package.json b/packages/api/package.json index 690da6b76..02a6f952d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -80,12 +80,12 @@ "@web5/agent": "0.3.1", "@web5/common": "1.0.0", "@web5/crypto": "1.0.0", - "@web5/dids": "1.0.0", + "@web5/dids": "1.0.1", "@web5/user-agent": "0.3.1" }, "devDependencies": { "@playwright/test": "1.40.1", - "@tbd54566975/dwn-sdk-js": "0.2.22", + "@tbd54566975/dwn-sdk-js": "0.3.1", "@types/chai": "4.3.6", "@types/eslint": "8.44.2", "@types/mocha": "10.0.1", diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 0de337695..9358ddf8a 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -9,7 +9,7 @@ import type { } from '@web5/agent'; import { DwnInterface } from '@web5/agent'; -import { Convert, NodeStream, removeUndefinedProperties, Stream } from '@web5/common'; +import { Convert, isEmptyObject, NodeStream, removeUndefinedProperties, Stream } from '@web5/common'; import { dataToBlob, SendCache } from './utils.js'; @@ -105,6 +105,9 @@ export type RecordUpdateParams = { /** The published status of the record. */ published?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['published']; + + /** The tags associated with the updated record */ + tags?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['tags']; } /** @@ -247,6 +250,8 @@ export class Record implements RecordModel { /** Record's published status (true/false) */ get published() { return this._descriptor.published; } + get tags() { return this._descriptor.tags; } + /** * Returns a copy of the raw `RecordsWriteMessage` that was used to create the current `Record` instance. */ @@ -540,7 +545,8 @@ export class Record implements RecordModel { published : this.published, recipient : this.recipient, recordId : this.id, - schema : this.schema + schema : this.schema, + tags : this.tags, }; } @@ -586,6 +592,13 @@ export class Record implements RecordModel { recordId : this._recordId }; + // NOTE: The original Record's tags are copied to the update message, so that the tags are not lost. + // However if a user passes new tags in the `RecordUpdateParams` object, they will overwrite the original tags. + // If the updated tag object is empty or set to null, we remove the tags property to avoid schema validation errors in the DWN SDK. + if (isEmptyObject(updateMessage.tags) || updateMessage.tags === null) { + delete updateMessage.tags; + } + let dataBlob: Blob; if (data !== undefined) { // If `data` is being updated then `dataCid` and `dataSize` must be undefined and the `data` @@ -598,7 +611,7 @@ export class Record implements RecordModel { // Throw an error if an attempt is made to modify immutable properties. // Note: `data` and `dateModified` have already been handled. - const mutableDescriptorProperties = new Set(['data', 'dataCid', 'dataSize', 'datePublished', 'messageTimestamp', 'published']); + const mutableDescriptorProperties = new Set(['data', 'dataCid', 'dataSize', 'datePublished', 'messageTimestamp', 'published', 'tags']); Record.verifyPermittedMutation(Object.keys(params), mutableDescriptorProperties); // If `published` is set to false, ensure that `datePublished` is undefined. Otherwise, DWN SDK's schema validation diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 3854c9313..125d27e76 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -205,8 +205,8 @@ describe('DwnApi', () => { const response = await dwnAlice.protocols.query({ from : bobDid.uri, message : { - permissionsGrantId : 'bafyreiduimprbncdo2oruvjrvmfmwuyz4xx3d5biegqd2qntlryvuuosem', - filter : { + permissionGrantId : 'bafyreiduimprbncdo2oruvjrvmfmwuyz4xx3d5biegqd2qntlryvuuosem', + filter : { protocol: 'https://doesnotexist.com/protocol' } } @@ -238,6 +238,32 @@ describe('DwnApi', () => { expect(await result.record?.data.text()).to.equal(dataString); }); + it('creates a record with tags', async () => { + const result = await dwnAlice.records.create({ + data : 'some data', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + foo : 'bar', + count : 2, + bool : true + } + } + }); + + expect(result.status.code).to.equal(202); + expect(result.status.detail).to.equal('Accepted'); + expect(result.record).to.exist; + expect(result.record?.tags).to.exist; + expect(result.record?.tags).to.deep.equal({ + foo : 'bar', + count : 2, + bool : true + }); + + }); + it('creates a record with JSON data', async () => { const dataJson = { hello: 'world!'}; const result = await dwnAlice.records.create({ @@ -841,6 +867,69 @@ describe('DwnApi', () => { expect(publishedDescResults.records!.length).to.equal(3); expect(publishedDescResults.records.map(r => r.id)).to.eql([...publishedItems].reverse()); }); + + it('queries for records matching tags', async () => { + + // Write a record to the agent's local DWN that includes a tag `foo` with value `bar` + const { status, record } = await dwnAlice.records.write({ + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + foo: 'bar', + } + } + }); + expect(status.code).to.equal(202); + + // Write a record to the agent's local DWN that includes a tag `foo` with value `baz` + const { status: status2 } = await dwnAlice.records.write({ + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + foo: 'baz', + } + } + }); + expect(status2.code).to.equal(202); + + // Control: query the agent's local DWN for the record without any tag filters + const result = await dwnAlice.records.query({ + message: { + filter: { + schema: 'foo/bar' + } + } + }); + + // should return both records + expect(result.status.code).to.equal(200); + expect(result.records).to.exist; + expect(result.records!.length).to.equal(2); + + + // Query the agent's local DWN for the record using the tags. + const fooBarResult = await dwnAlice.records.query({ + message: { + filter: { + schema : 'foo/bar', + tags : { + foo: 'bar', + } + } + } + }); + + // should only return the record with the tag `foo` and value `bar` + expect(fooBarResult.status.code).to.equal(200); + expect(fooBarResult.records).to.exist; + expect(fooBarResult.records!.length).to.equal(1); + expect(fooBarResult.records![0].id).to.equal(record.id); + expect(fooBarResult.records![0].tags).to.deep.equal({ foo: 'bar' }); + }); }); describe('from: did', () => { @@ -947,6 +1036,77 @@ describe('DwnApi', () => { const [ recordOnBobsDwn ] = bobQueryResult.records; expect(recordOnBobsDwn.author).to.equal(aliceDid.uri); }); + + it('queries for records matching tags', async () => { + + // Write a record to alice's remote DWN that includes a tag `foo` with value `bar` + const { status, record } = await dwnAlice.records.write({ + store : false, + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + foo: 'bar', + } + } + }); + expect(status.code).to.equal(202); + const { status: sendFooBarStatus } = await record.send(aliceDid.uri); + expect(sendFooBarStatus.code).to.equal(202); + + // Write a record to alice's remote DWN that includes a tag `foo` with value `baz` + const { status: status2, record: record2 } = await dwnAlice.records.write({ + store : false, + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + foo: 'baz', + } + } + }); + expect(status2.code).to.equal(202); + const { status: sendFooBazStatus } = await record2.send(aliceDid.uri); + expect(sendFooBazStatus.code).to.equal(202); + + // Control: query the agent's local DWN for the record without any tag filters + const result = await dwnAlice.records.query({ + from : aliceDid.uri, + message : { + filter: { + schema: 'foo/bar' + } + } + }); + + // should return both records + expect(result.status.code).to.equal(200); + expect(result.records).to.exist; + expect(result.records!.length).to.equal(2); + + + // Query the agent's local DWN for the record using the tags. + const fooBarResult = await dwnAlice.records.query({ + from : aliceDid.uri, + message : { + filter: { + schema : 'foo/bar', + tags : { + foo: 'bar', + } + } + } + }); + + // should only return the record with the tag `foo` and value `bar` + expect(fooBarResult.status.code).to.equal(200); + expect(fooBarResult.records).to.exist; + expect(fooBarResult.records!.length).to.equal(1); + expect(fooBarResult.records![0].id).to.equal(record.id); + expect(fooBarResult.records![0].tags).to.deep.equal({ foo: 'bar' }); + }); }); }); diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index feaa544ea..148313b6b 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -2366,6 +2366,94 @@ describe('Record', () => { expect(error.message).to.include('is an immutable property. Its value cannot be changed.'); } }); + + it('should override tags on update', async () => { + // create a record with tags + const { status, record } = await dwnAlice.records.write({ + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + tag1 : 'value1', + tag2 : 'value2' + } + } + }); + + expect(status.code).to.equal(202); + expect(record).to.not.be.undefined; + expect(await record.data.text()).to.equal('Hello, world!'); + expect(record.tags).to.deep.equal({ tag1: 'value1', tag2: 'value2'}); + + // if you do not pass any tags they remain unchanged + const updateResultWithoutTags = await record!.update({ + data: 'hi', + }); + + expect(updateResultWithoutTags.status.code).to.equal(202); + expect(record.tags).to.deep.equal({ tag1: 'value1', tag2: 'value2'}); // unchanged + expect(await record.data.text()).to.equal('hi'); + + // if you modify the tags they override the existing tags + const updateResultWithTags = await record!.update({ + tags: { + tag1 : 'value3', + tag3 : 'value4' + } + }); + + expect(updateResultWithTags.status.code).to.equal(202); + expect(record.tags).to.deep.equal({ tag1: 'value3', tag3: 'value4'}); // changed to updated tags + expect(await record.data.text()).to.equal('hi'); + }); + + it('should remove tags on update if tags are set to an empty object or null', async () => { + // create a record with tags + const { status, record } = await dwnAlice.records.write({ + data : 'Hello, world!', + message : { + schema : 'foo/bar', + dataFormat : 'text/plain', + tags : { + tag1 : 'value1', + tag2 : 'value2' + } + } + }); + + expect(status.code).to.equal(202); + expect(record).to.not.be.undefined; + expect(await record.data.text()).to.equal('Hello, world!'); + expect(record.tags).to.deep.equal({ tag1: 'value1', tag2: 'value2'}); + + // if you use an empty tags object it removes the tags + const updateResultWithEmptyTags = await record!.update({ + tags: {} + }); + + expect(updateResultWithEmptyTags.status.code).to.equal(202); + expect(record.tags).to.not.exist; // removed + + // add tags to the record again + const updateResultWithTags = await record!.update({ + tags: { + tag1 : 'value3', + tag3 : 'value4' + } + }); + + expect(updateResultWithTags.status.code).to.equal(202); + expect(record.tags).to.deep.equal({ tag1: 'value3', tag3: 'value4'}); // added tags + + // if you use null it removes the tags + const updateResultWithNullTags = await record!.update({ + tags: null + }); + + expect(updateResultWithNullTags.status.code).to.equal(202); + expect(record.tags).to.not.exist; // removed + }); }); describe('store()', () => { diff --git a/packages/dev-env/docker-compose.yaml b/packages/dev-env/docker-compose.yaml index f24672352..585e567d0 100644 --- a/packages/dev-env/docker-compose.yaml +++ b/packages/dev-env/docker-compose.yaml @@ -3,6 +3,6 @@ version: "3.98" services: dwn-server: container_name: dwn-server - image: ghcr.io/tbd54566975/dwn-server:dwn-sdk-0.2.22 + image: ghcr.io/tbd54566975/dwn-server:dwn-sdk-0.3.1 ports: - "3000:3000" diff --git a/packages/identity-agent/package.json b/packages/identity-agent/package.json index f4e5c8b79..5b4cc8dc4 100644 --- a/packages/identity-agent/package.json +++ b/packages/identity-agent/package.json @@ -72,7 +72,7 @@ "@web5/agent": "0.3.1", "@web5/common": "1.0.0", "@web5/crypto": "1.0.0", - "@web5/dids": "1.0.0" + "@web5/dids": "1.0.1" }, "devDependencies": { "@playwright/test": "1.40.1", diff --git a/packages/proxy-agent/package.json b/packages/proxy-agent/package.json index d723bba31..d3d549432 100644 --- a/packages/proxy-agent/package.json +++ b/packages/proxy-agent/package.json @@ -72,7 +72,7 @@ "@web5/agent": "0.3.1", "@web5/common": "1.0.0", "@web5/crypto": "1.0.0", - "@web5/dids": "1.0.0" + "@web5/dids": "1.0.1" }, "devDependencies": { "@playwright/test": "1.40.1", diff --git a/packages/user-agent/package.json b/packages/user-agent/package.json index 648f67815..e098e93c6 100644 --- a/packages/user-agent/package.json +++ b/packages/user-agent/package.json @@ -72,7 +72,7 @@ "@web5/agent": "0.3.1", "@web5/common": "1.0.0", "@web5/crypto": "1.0.0", - "@web5/dids": "1.0.0" + "@web5/dids": "1.0.1" }, "devDependencies": { "@playwright/test": "1.40.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da1977829..1da0d6cbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: 6.4.0 version: 6.4.0(@typescript-eslint/parser@6.4.0)(eslint@8.47.0)(typescript@5.1.6) '@web5/dwn-server': - specifier: 0.1.17 - version: 0.1.17 + specifier: 0.2.1 + version: 0.2.1 eslint-plugin-mocha: specifier: 10.1.0 version: 10.1.0(eslint@8.47.0) @@ -42,8 +42,8 @@ importers: specifier: 1.2.2 version: 1.2.2 '@tbd54566975/dwn-sdk-js': - specifier: 0.2.22 - version: 0.2.22 + specifier: 0.3.1 + version: 0.3.1 '@web5/common': specifier: 1.0.0 version: link:../common @@ -51,8 +51,8 @@ importers: specifier: 1.0.0 version: link:../crypto '@web5/dids': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: link:../dids abstract-level: specifier: 1.0.4 version: 1.0.4 @@ -157,8 +157,8 @@ importers: specifier: 1.0.0 version: link:../crypto '@web5/dids': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: link:../dids '@web5/user-agent': specifier: 0.3.1 version: link:../user-agent @@ -167,8 +167,8 @@ importers: specifier: 1.40.1 version: 1.40.1 '@tbd54566975/dwn-sdk-js': - specifier: 0.2.22 - version: 0.2.22 + specifier: 0.3.1 + version: 0.3.1 '@types/chai': specifier: 4.3.6 version: 4.3.6 @@ -691,8 +691,8 @@ importers: specifier: 1.0.0 version: link:../crypto '@web5/dids': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: link:../dids devDependencies: '@playwright/test': specifier: 1.40.1 @@ -779,8 +779,8 @@ importers: specifier: 1.0.0 version: link:../crypto '@web5/dids': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: link:../dids devDependencies: '@playwright/test': specifier: 1.40.1 @@ -864,8 +864,8 @@ importers: specifier: 1.0.0 version: link:../crypto '@web5/dids': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: link:../dids devDependencies: '@playwright/test': specifier: 1.40.1 @@ -2755,15 +2755,15 @@ packages: jwt-decode: 3.1.2 dev: true - /@tbd54566975/dwn-sdk-js@0.2.22: - resolution: {integrity: sha512-TBobNAWt09bsAKADiiWNcdgiuuWNkHAumPvuYM9d+V/Brcl99Q9jg3ssVQhMfhV3TN8zxCbAGWYALUfxgX4N3w==} + /@tbd54566975/dwn-sdk-js@0.3.1: + resolution: {integrity: sha512-G73ixUGieRBE4kYxLlYu/9wN36RUBGp5nvDrlAWZsNUXEXWnhCS3qTYFnUqzPEXjoE0z8EVRtpMU/eITM8ZcDA==} engines: {node: '>= 18'} dependencies: '@ipld/dag-cbor': 9.0.3 '@js-temporal/polyfill': 0.4.4 '@noble/ed25519': 2.0.0 '@noble/secp256k1': 2.0.0 - '@web5/dids': 1.0.0 + '@web5/dids': 1.0.1 abstract-level: 1.0.3 ajv: 8.12.0 blockstore-core: 4.2.0 @@ -2787,12 +2787,12 @@ packages: - encoding - supports-color - /@tbd54566975/dwn-sql-store@0.2.13: - resolution: {integrity: sha512-TYl16RwExcasH1UTNEQDwcGJbDA96hYQ4iVdpJc7xVfwGwtLzJWR1jSkCugP3BbLJ7lPPy0pywysZ1fW4b/bJg==} + /@tbd54566975/dwn-sql-store@0.4.1: + resolution: {integrity: sha512-ndslsbtNjkIuNu8ytNZnKjH4uWoxWFzt+L/8ok5giVmgrjTh/+XDU23LQYJjRHC/RusjpDNjlt77PhL2qbXxmQ==} engines: {node: '>=18'} dependencies: '@ipld/dag-cbor': 9.2.0 - '@tbd54566975/dwn-sdk-js': 0.2.22 + '@tbd54566975/dwn-sdk-js': 0.3.1 kysely: 0.26.3 multiformats: 12.0.1 readable-stream: 4.4.2 @@ -3463,13 +3463,28 @@ packages: buffer: 6.0.3 level: 8.0.0 ms: 2.1.3 + dev: false + + /@web5/dids@1.0.1: + resolution: {integrity: sha512-bAc+zwTDPvtFtd8T25XD0oUmSOBmeTpYSZyBz9w/EqZPKtZOFSc5oFS5qLtrh4YDkkcBqTG5ENQlE9fXs56zIQ==} + engines: {node: '>=18.0.0'} + dependencies: + '@decentralized-identity/ion-sdk': 1.0.1 + '@dnsquery/dns-packet': 6.1.1 + '@web5/common': 1.0.0 + '@web5/crypto': 1.0.0 + abstract-level: 1.0.4 + bencode: 4.0.0 + buffer: 6.0.3 + level: 8.0.0 + ms: 2.1.3 - /@web5/dwn-server@0.1.17: - resolution: {integrity: sha512-JkcZ4dQCZi6fCbEdyqqVqjDKE/AbaJj0auppgSnV5XxqjhKOtelvm+lLtppzyhcuPStlrem/9LkXR7yIgJ/JhQ==} + /@web5/dwn-server@0.2.1: + resolution: {integrity: sha512-RMt+YjVF3qru6zpjIdqB9vlCBfBt+H7QrrE/VsX8/VoQVRdrRPLnMIUVeYnfOn1pjXMWQs8cZjBypD04lMYZCA==} hasBin: true dependencies: - '@tbd54566975/dwn-sdk-js': 0.2.22 - '@tbd54566975/dwn-sql-store': 0.2.13 + '@tbd54566975/dwn-sdk-js': 0.3.1 + '@tbd54566975/dwn-sql-store': 0.4.1 better-sqlite3: 8.7.0 body-parser: 1.20.2 bytes: 3.1.2 @@ -3479,7 +3494,7 @@ packages: loglevel: 1.9.1 loglevel-plugin-prefix: 0.8.4 multiformats: 11.0.2 - mysql2: 3.9.1 + mysql2: 3.9.7 node-fetch: 3.3.1 pg: 8.11.3 pg-cursor: 2.10.3(pg@8.11.3) @@ -7070,8 +7085,8 @@ packages: resolution: {integrity: sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g==} engines: {node: '>=8.0.0'} - /mysql2@3.9.1: - resolution: {integrity: sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==} + /mysql2@3.9.7: + resolution: {integrity: sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==} engines: {node: '>= 8.0'} dependencies: denque: 2.1.0