From a319800516cd2c4aeb67be857a7d3696aeb4af27 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Tue, 1 Oct 2024 16:16:05 -0400 Subject: [PATCH 01/11] make sure protocolRole persists between requests --- packages/api/src/dwn-api.ts | 3 +++ packages/api/src/record.ts | 2 +- packages/api/src/subscription-util.ts | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index e91f21622..a0d5b2028 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -699,6 +699,7 @@ export class DwnApi { */ remoteOrigin : request.from, delegateDid : this.delegateDid, + protocolRole: request.message.protocolRole, ...entry as DwnMessage[DwnInterface.RecordsWrite] }; const record = new Record(this.agent, recordOptions, this.permissionsApi); @@ -787,6 +788,7 @@ export class DwnApi { * payload must be read again (e.g., if the data stream is consumed). */ remoteOrigin : request.from, + protocolRole: request.message.protocolRole, delegateDid : this.delegateDid, data : entry.data, initialWrite : entry.initialWrite, @@ -829,6 +831,7 @@ export class DwnApi { connectedDid : this.connectedDid, delegateDid : this.delegateDid, permissionsApi : this.permissionsApi, + protocolRole : request.message.protocolRole, request }) }; diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 47b703c24..306f53656 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -1023,7 +1023,7 @@ export class Record implements RecordModel { private async readRecordData({ target, isRemote }: { target: string, isRemote: boolean }) { const readRequest: ProcessDwnRequest = { author : this._connectedDid, - messageParams : { filter: { recordId: this.id } }, + messageParams : { filter: { recordId: this.id }, protocolRole: this._protocolRole }, messageType : DwnInterface.RecordsRead, target, }; diff --git a/packages/api/src/subscription-util.ts b/packages/api/src/subscription-util.ts index 5316733d0..88a6f16d2 100644 --- a/packages/api/src/subscription-util.ts +++ b/packages/api/src/subscription-util.ts @@ -9,10 +9,11 @@ export class SubscriptionUtil { /** * Creates a record subscription handler that can be used to process incoming {Record} messages. */ - static recordSubscriptionHandler({ agent, connectedDid, request, delegateDid, permissionsApi }:{ + static recordSubscriptionHandler({ agent, connectedDid, request, delegateDid, protocolRole, permissionsApi }:{ agent: Web5Agent; connectedDid: string; delegateDid?: string; + protocolRole?: string; permissionsApi?: PermissionsApi; request: RecordsSubscribeRequest; }): DwnRecordSubscriptionHandler { @@ -31,6 +32,7 @@ export class SubscriptionUtil { const record = new Record(agent, { ...message, ...recordOptions, + protocolRole, delegateDid: delegateDid, }, permissionsApi); From adbfeb68af2bc4e95715a28e8f5da6d2595bc9b4 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Tue, 1 Oct 2024 16:20:50 -0400 Subject: [PATCH 02/11] fix linting issue --- packages/api/src/dwn-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index a0d5b2028..9ba19d57c 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -699,7 +699,7 @@ export class DwnApi { */ remoteOrigin : request.from, delegateDid : this.delegateDid, - protocolRole: request.message.protocolRole, + protocolRole : request.message.protocolRole, ...entry as DwnMessage[DwnInterface.RecordsWrite] }; const record = new Record(this.agent, recordOptions, this.permissionsApi); @@ -788,7 +788,7 @@ export class DwnApi { * payload must be read again (e.g., if the data stream is consumed). */ remoteOrigin : request.from, - protocolRole: request.message.protocolRole, + protocolRole : request.message.protocolRole, delegateDid : this.delegateDid, data : entry.data, initialWrite : entry.initialWrite, From d7a6958541301a978b8bffabcaebf267690d9f14 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Thu, 17 Oct 2024 17:27:09 -0400 Subject: [PATCH 03/11] instatiate record object with protocol role and use it when updating/reading/querying --- packages/api/src/dwn-api.ts | 2 - packages/api/src/record.ts | 5 + packages/api/tests/dwn-api.spec.ts | 119 +++++++++++++++++- .../fixtures/protocol-definitions/photos.json | 6 +- 4 files changed, 126 insertions(+), 6 deletions(-) diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 9ba19d57c..37988ca70 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -788,7 +788,6 @@ export class DwnApi { * payload must be read again (e.g., if the data stream is consumed). */ remoteOrigin : request.from, - protocolRole : request.message.protocolRole, delegateDid : this.delegateDid, data : entry.data, initialWrite : entry.initialWrite, @@ -831,7 +830,6 @@ export class DwnApi { connectedDid : this.connectedDid, delegateDid : this.delegateDid, permissionsApi : this.permissionsApi, - protocolRole : request.message.protocolRole, request }) }; diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 306f53656..f0a67e376 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -183,6 +183,9 @@ export type RecordDeleteParams = { /** The timestamp indicating when the record was deleted. */ dateModified?: DwnMessageDescriptor[DwnInterface.RecordsDelete]['messageTimestamp']; + + /** The protocol role under which this record will be deleted. */ + protocolRole?: RecordOptions['protocolRole']; }; /** @@ -353,12 +356,14 @@ export class Record implements RecordModel { descriptor : this._descriptor, attestation : this._attestation, authorization : this._authorization, + protocolRole : this._protocolRole, encryption : this._encryption, })); } else { message = JSON.parse(JSON.stringify({ descriptor : this._descriptor, authorization : this._authorization, + protocolRole : this._protocolRole, })); } diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 7f332c000..fd0043b8c 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -9,7 +9,7 @@ import { DwnApi } from '../src/dwn-api.js'; import { testDwnUrl } from './utils/test-config.js'; import emailProtocolDefinition from './fixtures/protocol-definitions/email.json' assert { type: 'json' }; import photosProtocolDefinition from './fixtures/protocol-definitions/photos.json' assert { type: 'json' }; -import { DwnInterfaceName, DwnMethodName, Jws, PermissionsProtocol, Poller, Time } from '@tbd54566975/dwn-sdk-js'; +import { DwnConstant, DwnInterfaceName, DwnMethodName, Jws, PermissionsProtocol, Poller, Time } from '@tbd54566975/dwn-sdk-js'; import { PermissionGrant } from '../src/permission-grant.js'; import { Record } from '../src/record.js'; import { TestDataGenerator } from './utils/test-data-generator.js'; @@ -1055,6 +1055,123 @@ describe('DwnApi', () => { expect(await result.record?.data.json()).to.deep.equal(dataJson); }); + it('ensure that a protocolRole used to query is also used to read the data of the result', async () => { + // Configure the photos protocol on Alice and Bob's local and remote DWNs. + const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ + message: { + definition: photosProtocolDefinition + } + }); + expect(bobProtocolStatus.code).to.equal(202); + const { status: bobRemoteProtocolStatus } = await bobProtocol.send(bobDid.uri); + expect(bobRemoteProtocolStatus.code).to.equal(202); + + // Bob creates an album + const { status: albumCreateStatus, record: albumRecord } = await dwnBob.records.create({ + data : 'My Album', + message : { + protocol : photosProtocolDefinition.protocol, + protocolPath : 'album', + schema : photosProtocolDefinition.types.album.schema, + dataFormat : 'text/plain' + } + }); + expect(albumCreateStatus.code).to.equal(202); + const { status: albumSendStatus } = await albumRecord.send(); + expect(albumSendStatus.code).to.equal(202); + + // Bob makes Alice a `participant` and sends the record to her and his own remote node. + const { status: participantCreateStatus, record: participantRecord} = await dwnBob.records.create({ + data : 'test', + message : { + parentContextId : albumRecord.contextId, + recipient : aliceDid.uri, + protocol : photosProtocolDefinition.protocol, + protocolPath : 'album/participant', + schema : photosProtocolDefinition.types.participant.schema, + dataFormat : 'text/plain' + } + }); + expect(participantCreateStatus.code).to.equal(202); + const { status: bobParticipantSendStatus } = await participantRecord.send(bobDid.uri); + expect(bobParticipantSendStatus.code).to.equal(202); + + // bob adds 3 photos to the album + for (let i = 0; i < 3; i++) { + const { status: photoCreateStatus, record: photoRecord } = await dwnBob.records.create({ + data : TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1), + message : { + parentContextId : albumRecord.contextId, + protocol : photosProtocolDefinition.protocol, + protocolPath : 'album/photo', + schema : photosProtocolDefinition.types.photo.schema, + dataFormat : 'text/plain', + } + }); + expect(photoCreateStatus.code).to.equal(202); + const { status: photoSendStatus } = await photoRecord.send(); + expect(photoSendStatus.code).to.equal(202); + } + + // alice uses the role to add a photo to the album + const { status: photoCreateStatusAlice, record: photoRecordAlice } = await dwnAlice.records.create({ + store : false, + data : TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1), + message : { + parentContextId : albumRecord.contextId, + protocol : photosProtocolDefinition.protocol, + protocolPath : 'album/photo', + protocolRole : 'album/participant', + schema : photosProtocolDefinition.types.photo.schema, + dataFormat : 'text/plain' + } + }); + expect(photoCreateStatusAlice.code).to.equal(202); + const { status: albumSendStatusAlice } = await photoRecordAlice.send(bobDid.uri); + expect(albumSendStatusAlice.code).to.equal(202); + + //SANITY: Alice attempts to fetch the photos without the role, she should only see her own photo + const { status: alicePhotosReadResultWithoutRole, records: alicePhotosRecordsWithoutRole } = await dwnAlice.records.query({ + from : bobDid.uri, + message : { + filter: { + protocol : photosProtocolDefinition.protocol, + protocolPath : 'album/photo', + contextId : albumRecord.contextId + } + } + }); + expect(alicePhotosReadResultWithoutRole.code).to.equal(200); + expect(alicePhotosRecordsWithoutRole).to.exist; + expect(alicePhotosRecordsWithoutRole).to.have.lengthOf(1); + + // Attempt to read the data of the photo, which should succeed + const readResultWithoutRole = await alicePhotosRecordsWithoutRole[0].data.text(); + expect(readResultWithoutRole.length).to.equal(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); + + // Alice fetches all of the photos from the album + const alicePhotosReadResult = await dwnAlice.records.query({ + from : bobDid.uri, + message : { + protocolRole : 'album/participant', + filter : { + protocol : photosProtocolDefinition.protocol, + protocolPath : 'album/photo', + contextId : albumRecord.contextId + } + } + }); + expect(alicePhotosReadResult.status.code).to.equal(200); + expect(alicePhotosReadResult.records).to.exist; + expect(alicePhotosReadResult.records).to.have.lengthOf(4); + + // attempt to read data from the photos + for (const record of alicePhotosReadResult.records) { + const readResult = await record.data.text(); + expect(readResult.length).to.equal(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); + } + }); + it('creates a role record for another user that they can use to create role-based records', async () => { /** * WHAT IS BEING TESTED? diff --git a/packages/api/tests/fixtures/protocol-definitions/photos.json b/packages/api/tests/fixtures/protocol-definitions/photos.json index 4a5c6c4ca..dfa3083dd 100644 --- a/packages/api/tests/fixtures/protocol-definitions/photos.json +++ b/packages/api/tests/fixtures/protocol-definitions/photos.json @@ -32,7 +32,7 @@ { "role": "friend", "can": [ - "create", "update" + "create", "update", "read", "query", "subscribe" ] } ], @@ -54,7 +54,7 @@ { "role": "album/participant", "can": [ - "create", "update" + "create", "update", "read", "query", "subscribe" ] } ] @@ -64,7 +64,7 @@ { "role": "album/participant", "can": [ - "create", "update" + "create", "update", "read", "query", "subscribe" ] }, { From 0a53308315054e00f47681b1aa9525765fdcc502 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 18 Oct 2024 10:58:54 -0400 Subject: [PATCH 04/11] test query and subscribe ability to read data of records --- packages/api/src/dwn-api.ts | 1 + packages/api/tests/dwn-api.spec.ts | 267 ++++++++++-------- .../fixtures/protocol-definitions/notes.json | 48 ++++ .../fixtures/protocol-definitions/photos.json | 6 +- 4 files changed, 202 insertions(+), 120 deletions(-) create mode 100644 packages/api/tests/fixtures/protocol-definitions/notes.json diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index 37988ca70..bcc481358 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -830,6 +830,7 @@ export class DwnApi { connectedDid : this.connectedDid, delegateDid : this.delegateDid, permissionsApi : this.permissionsApi, + protocolRole : request.message.protocolRole, request }) }; diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index fd0043b8c..abb479608 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -9,6 +9,7 @@ import { DwnApi } from '../src/dwn-api.js'; import { testDwnUrl } from './utils/test-config.js'; import emailProtocolDefinition from './fixtures/protocol-definitions/email.json' assert { type: 'json' }; import photosProtocolDefinition from './fixtures/protocol-definitions/photos.json' assert { type: 'json' }; +import notesProtocolDefinition from './fixtures/protocol-definitions/notes.json' assert { type: 'json' }; import { DwnConstant, DwnInterfaceName, DwnMethodName, Jws, PermissionsProtocol, Poller, Time } from '@tbd54566975/dwn-sdk-js'; import { PermissionGrant } from '../src/permission-grant.js'; import { Record } from '../src/record.js'; @@ -1055,123 +1056,6 @@ describe('DwnApi', () => { expect(await result.record?.data.json()).to.deep.equal(dataJson); }); - it('ensure that a protocolRole used to query is also used to read the data of the result', async () => { - // Configure the photos protocol on Alice and Bob's local and remote DWNs. - const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ - message: { - definition: photosProtocolDefinition - } - }); - expect(bobProtocolStatus.code).to.equal(202); - const { status: bobRemoteProtocolStatus } = await bobProtocol.send(bobDid.uri); - expect(bobRemoteProtocolStatus.code).to.equal(202); - - // Bob creates an album - const { status: albumCreateStatus, record: albumRecord } = await dwnBob.records.create({ - data : 'My Album', - message : { - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album', - schema : photosProtocolDefinition.types.album.schema, - dataFormat : 'text/plain' - } - }); - expect(albumCreateStatus.code).to.equal(202); - const { status: albumSendStatus } = await albumRecord.send(); - expect(albumSendStatus.code).to.equal(202); - - // Bob makes Alice a `participant` and sends the record to her and his own remote node. - const { status: participantCreateStatus, record: participantRecord} = await dwnBob.records.create({ - data : 'test', - message : { - parentContextId : albumRecord.contextId, - recipient : aliceDid.uri, - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album/participant', - schema : photosProtocolDefinition.types.participant.schema, - dataFormat : 'text/plain' - } - }); - expect(participantCreateStatus.code).to.equal(202); - const { status: bobParticipantSendStatus } = await participantRecord.send(bobDid.uri); - expect(bobParticipantSendStatus.code).to.equal(202); - - // bob adds 3 photos to the album - for (let i = 0; i < 3; i++) { - const { status: photoCreateStatus, record: photoRecord } = await dwnBob.records.create({ - data : TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1), - message : { - parentContextId : albumRecord.contextId, - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album/photo', - schema : photosProtocolDefinition.types.photo.schema, - dataFormat : 'text/plain', - } - }); - expect(photoCreateStatus.code).to.equal(202); - const { status: photoSendStatus } = await photoRecord.send(); - expect(photoSendStatus.code).to.equal(202); - } - - // alice uses the role to add a photo to the album - const { status: photoCreateStatusAlice, record: photoRecordAlice } = await dwnAlice.records.create({ - store : false, - data : TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1), - message : { - parentContextId : albumRecord.contextId, - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album/photo', - protocolRole : 'album/participant', - schema : photosProtocolDefinition.types.photo.schema, - dataFormat : 'text/plain' - } - }); - expect(photoCreateStatusAlice.code).to.equal(202); - const { status: albumSendStatusAlice } = await photoRecordAlice.send(bobDid.uri); - expect(albumSendStatusAlice.code).to.equal(202); - - //SANITY: Alice attempts to fetch the photos without the role, she should only see her own photo - const { status: alicePhotosReadResultWithoutRole, records: alicePhotosRecordsWithoutRole } = await dwnAlice.records.query({ - from : bobDid.uri, - message : { - filter: { - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album/photo', - contextId : albumRecord.contextId - } - } - }); - expect(alicePhotosReadResultWithoutRole.code).to.equal(200); - expect(alicePhotosRecordsWithoutRole).to.exist; - expect(alicePhotosRecordsWithoutRole).to.have.lengthOf(1); - - // Attempt to read the data of the photo, which should succeed - const readResultWithoutRole = await alicePhotosRecordsWithoutRole[0].data.text(); - expect(readResultWithoutRole.length).to.equal(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); - - // Alice fetches all of the photos from the album - const alicePhotosReadResult = await dwnAlice.records.query({ - from : bobDid.uri, - message : { - protocolRole : 'album/participant', - filter : { - protocol : photosProtocolDefinition.protocol, - protocolPath : 'album/photo', - contextId : albumRecord.contextId - } - } - }); - expect(alicePhotosReadResult.status.code).to.equal(200); - expect(alicePhotosReadResult.records).to.exist; - expect(alicePhotosReadResult.records).to.have.lengthOf(4); - - // attempt to read data from the photos - for (const record of alicePhotosReadResult.records) { - const readResult = await record.data.text(); - expect(readResult.length).to.equal(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); - } - }); - it('creates a role record for another user that they can use to create role-based records', async () => { /** * WHAT IS BEING TESTED? @@ -2196,6 +2080,75 @@ describe('DwnApi', () => { expect(fooBarResult.records![0].id).to.equal(record.id); expect(fooBarResult.records![0].tags).to.deep.equal({ foo: 'bar' }); }); + + it('ensures that a protocolRole used to query is also used to read the data of the resulted records', async () => { + // Bob configures the notes protocol for himself + const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ + message: { + definition: notesProtocolDefinition + } + }); + expect(bobProtocolStatus.code).to.equal(202); + const { status: bobRemoteProtocolStatus } = await bobProtocol.send(bobDid.uri); + expect(bobRemoteProtocolStatus.code).to.equal(202); + + // Bob creates a few notes ensuring that the data is larger than the max encoded size + // that way the data will be requested with a separate `read` request + const recordData: Map = new Map(); + for (let i = 0; i < 3; i++) { + const data = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); + const { status: noteCreateStatus, record: noteRecord } = await dwnBob.records.create({ + data, + message: { + protocol : notesProtocolDefinition.protocol, + protocolPath : 'note', + schema : notesProtocolDefinition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(noteCreateStatus.code).to.equal(202); + const { status: noteSendStatus } = await noteRecord.send(); + expect(noteSendStatus.code).to.equal(202); + recordData.set(noteRecord.id, data); + } + + // Bob makes Alice a `friend` to allow her to read and comment on his notes + const { status: friendCreateStatus, record: friendRecord} = await dwnBob.records.create({ + data : 'friend!', + message : { + recipient : aliceDid.uri, + protocol : notesProtocolDefinition.protocol, + protocolPath : 'friend', + schema : notesProtocolDefinition.types.friend.schema, + dataFormat : 'text/plain' + } + }); + expect(friendCreateStatus.code).to.equal(202); + const { status: bobFriendSendStatus } = await friendRecord.send(bobDid.uri); + expect(bobFriendSendStatus.code).to.equal(202); + + // alice uses the role to query for the available notes + const { status: notesQueryStatus, records: noteRecords } = await dwnAlice.records.query({ + from : bobDid.uri, + message : { + protocolRole : 'friend', + filter : { + protocol : notesProtocolDefinition.protocol, + protocolPath : 'note' + } + } + }); + expect(notesQueryStatus.code).to.equal(200); + expect(noteRecords).to.exist; + expect(noteRecords).to.have.lengthOf(3); + + // Alice attempts to read the data of the notes, which should succeed + for (const record of noteRecords) { + const readResult = await record.data.text(); + const expectedData = recordData.get(record.id); + expect(readResult).to.equal(expectedData); + } + }); }); }); @@ -2562,6 +2515,86 @@ describe('DwnApi', () => { expect(record.deleted).to.be.false; }); }); + + it('ensures that a protocolRole used to subscribe is also used to read the data of the resulted records', async () => { + // Bob configures the notes protocol for himself + const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ + message: { + definition: notesProtocolDefinition + } + }); + expect(bobProtocolStatus.code).to.equal(202); + const { status: bobRemoteProtocolStatus } = await bobProtocol.send(bobDid.uri); + expect(bobRemoteProtocolStatus.code).to.equal(202); + + + // Bob makes Alice a `friend` to allow her to read and comment on his notes + const { status: friendCreateStatus, record: friendRecord} = await dwnBob.records.create({ + data : 'friend!', + message : { + recipient : aliceDid.uri, + protocol : notesProtocolDefinition.protocol, + protocolPath : 'friend', + schema : notesProtocolDefinition.types.friend.schema, + dataFormat : 'text/plain' + } + }); + expect(friendCreateStatus.code).to.equal(202); + const { status: bobFriendSendStatus } = await friendRecord.send(bobDid.uri); + expect(bobFriendSendStatus.code).to.equal(202); + + // Alice subscribes to the notes protocol using the role + const notes: Map = new Map(); + const subscriptionHandler = async (record: Record) => { + notes.set(record.id, record); + }; + + // alice uses the role to query for the available notes + const { status: notesSubscribeStatus, subscription } = await dwnAlice.records.subscribe({ + from : bobDid.uri, + message : { + protocolRole : 'friend', + filter : { + protocol : notesProtocolDefinition.protocol, + protocolPath : 'note' + } + }, + subscriptionHandler + }); + expect(notesSubscribeStatus.code).to.equal(200); + expect(subscription).to.exist; + + // Bob creates a few notes ensuring that the data is larger than the max encoded size + // that way the data will be requested with a separate `read` request + const recordData: Map = new Map(); + for (let i = 0; i < 3; i++) { + const data = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); + const { status: noteCreateStatus, record: noteRecord } = await dwnBob.records.create({ + data, + message: { + protocol : notesProtocolDefinition.protocol, + protocolPath : 'note', + schema : notesProtocolDefinition.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(noteCreateStatus.code).to.equal(202); + const { status: noteSendStatus } = await noteRecord.send(); + expect(noteSendStatus.code).to.equal(202); + recordData.set(noteRecord.id, data); + } + + // poll for the note records to be received + await Poller.pollUntilSuccessOrTimeout(async () => { + expect(notes.size).to.equal(3); + }); + + for (const record of notes.values()) { + const readResult = await record.data.text(); + const expectedData = recordData.get(record.id); + expect(readResult).to.equal(expectedData); + } + }); }); }); diff --git a/packages/api/tests/fixtures/protocol-definitions/notes.json b/packages/api/tests/fixtures/protocol-definitions/notes.json new file mode 100644 index 000000000..a98acf917 --- /dev/null +++ b/packages/api/tests/fixtures/protocol-definitions/notes.json @@ -0,0 +1,48 @@ +{ + "protocol": "http://notes-protocol.xyz", + "published": true, + "types": { + "note": { + "schema": "http://notes-protocol.xyz/schema/note", + "dataFormats": [ + "text/plain", + "application/json" + ] + }, + "comment": { + "schema": "http://notes-protocol.xyz/schema/comment", + "dataFormats": [ + "text/plain", + "application/json" + ] + }, + "friend" : { + "schema": "http://notes-protocol.xyz/schema/friend", + "dataFormats": [ + "text/plain", + "application/json" + ] + } + }, + "structure": { + "friend" :{ + "$role": true + }, + "note": { + "$actions": [ + { + "role": "friend", + "can": ["read", "query", "subscribe"] + } + ], + "comment": { + "$actions": [ + { + "role": "friend", + "can": ["create", "delete", "read", "query", "subscribe"] + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/api/tests/fixtures/protocol-definitions/photos.json b/packages/api/tests/fixtures/protocol-definitions/photos.json index dfa3083dd..4a5c6c4ca 100644 --- a/packages/api/tests/fixtures/protocol-definitions/photos.json +++ b/packages/api/tests/fixtures/protocol-definitions/photos.json @@ -32,7 +32,7 @@ { "role": "friend", "can": [ - "create", "update", "read", "query", "subscribe" + "create", "update" ] } ], @@ -54,7 +54,7 @@ { "role": "album/participant", "can": [ - "create", "update", "read", "query", "subscribe" + "create", "update" ] } ] @@ -64,7 +64,7 @@ { "role": "album/participant", "can": [ - "create", "update", "read", "query", "subscribe" + "create", "update" ] }, { From 158c3b2bb8d9fc780f7163f6434575cc886198af Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 18 Oct 2024 17:31:47 -0400 Subject: [PATCH 05/11] additional test coverage for roles cases --- packages/api/src/record.ts | 29 +-- packages/api/tests/dwn-api.spec.ts | 34 ++- .../fixtures/protocol-definitions/notes.json | 19 +- packages/api/tests/record.spec.ts | 236 +++++++++++++++++- 4 files changed, 287 insertions(+), 31 deletions(-) diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index f0a67e376..ffb5451d8 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -61,9 +61,6 @@ export type RecordModel = ImmutableRecordProperties & OptionalRecordProperties & /** The timestamp indicating when the record was last modified. */ messageTimestamp?: string; - - /** The protocol role under which this record is written. */ - protocolRole?: RecordOptions['protocolRole']; } /** @@ -153,7 +150,7 @@ export type RecordUpdateParams = { datePublished?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['datePublished']; /** The protocol role under which this record is written. */ - protocolRole?: RecordOptions['protocolRole']; + protocolRole?: string; /** The published status of the record. */ published?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['published']; @@ -185,7 +182,7 @@ export type RecordDeleteParams = { dateModified?: DwnMessageDescriptor[DwnInterface.RecordsDelete]['messageTimestamp']; /** The protocol role under which this record will be deleted. */ - protocolRole?: RecordOptions['protocolRole']; + protocolRole?: string; }; /** @@ -227,6 +224,8 @@ export class Record implements RecordModel { private _readableStream?: Readable; /** The origin DID if the record was fetched from a remote DWN. */ private _remoteOrigin?: string; + /** The protocolRole to use when reading the record */ + private _protocolRole?: string; // Private variables for DWN `RecordsWrite` message properties. @@ -252,8 +251,6 @@ export class Record implements RecordModel { private _initialWriteSigned: boolean; /** Unique identifier of the record. */ private _recordId: string; - /** Role under which the record is written. */ - private _protocolRole?: RecordOptions['protocolRole']; /** The `RecordsWriteMessage` descriptor unless the record is in a deleted state */ private get _recordsWriteDescriptor() { @@ -314,7 +311,6 @@ export class Record implements RecordModel { /** Tags of the record */ get tags() { return this._recordsWriteDescriptor?.tags; } - // Getters for for properties that depend on the current state of the Record. /** DID that is the logical author of the Record. */ get author(): string { return this._author; } @@ -334,9 +330,6 @@ export class Record implements RecordModel { /** Record's signatures attestation */ get attestation(): DwnMessage[DwnInterface.RecordsWrite]['attestation'] | undefined { return this._attestation; } - /** Role under which the author is writing the record */ - get protocolRole() { return this._protocolRole; } - /** Record's deleted state (true/false) */ get deleted() { return isDwnMessage(DwnInterface.RecordsDelete, this.rawMessage); } @@ -356,14 +349,12 @@ export class Record implements RecordModel { descriptor : this._descriptor, attestation : this._attestation, authorization : this._authorization, - protocolRole : this._protocolRole, encryption : this._encryption, })); } else { message = JSON.parse(JSON.stringify({ descriptor : this._descriptor, authorization : this._authorization, - protocolRole : this._protocolRole, })); } @@ -656,7 +647,6 @@ export class Record implements RecordModel { parentId : this.parentId, protocol : this.protocol, protocolPath : this.protocolPath, - protocolRole : this.protocolRole, published : this.published, recipient : this.recipient, recordId : this.id, @@ -708,7 +698,7 @@ export class Record implements RecordModel { * * @beta */ - async update({ dateModified, data, ...params }: RecordUpdateParams): Promise { + async update({ dateModified, data, protocolRole, ...params }: RecordUpdateParams): Promise { if (this.deleted) { throw new Error('Record: Cannot revive a deleted record.'); @@ -723,6 +713,7 @@ export class Record implements RecordModel { ...descriptor, ...params, parentContextId, + protocolRole, messageTimestamp : dateModified, // Map Record class `dateModified` property to DWN SDK `messageTimestamp` recordId : this._recordId }; @@ -791,7 +782,6 @@ export class Record implements RecordModel { // Only update the local Record instance mutable properties if the record was successfully (over)written. this._authorization = responseMessage.authorization; - this._protocolRole = params.protocolRole; mutableDescriptorProperties.forEach(property => { this._descriptor[property] = responseMessage.descriptor[property]; }); @@ -839,7 +829,11 @@ export class Record implements RecordModel { store }; - if (this.deleted) { + // Check to see if the provided protocolRole is different from the current protocolRole + // If so we need to construct a delete message with the new protocolRole, otherwise we can use the existing + // NOTE: currently this is testing the instance _protocolRole, not the actual signature payload. + const differentRole = deleteParams?.protocolRole ? this._protocolRole !== deleteParams.protocolRole : false; + if (this.deleted && !differentRole) { // if we have a delete message we can just use it deleteOptions.rawMessage = this.rawMessage as DwnMessage[DwnInterface.RecordsDelete]; } else { @@ -848,6 +842,7 @@ export class Record implements RecordModel { prune : prune, recordId : this._recordId, messageTimestamp : dateModified, + protocolRole : deleteParams?.protocolRole }; } diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index abb479608..7ee55cc0e 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -2082,10 +2082,15 @@ describe('DwnApi', () => { }); it('ensures that a protocolRole used to query is also used to read the data of the resulted records', async () => { + const protocol = { + ...notesProtocolDefinition, + protocol: 'http://example.com/notes' + TestDataGenerator.randomString(15) + }; + // Bob configures the notes protocol for himself const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ message: { - definition: notesProtocolDefinition + definition: protocol } }); expect(bobProtocolStatus.code).to.equal(202); @@ -2100,9 +2105,9 @@ describe('DwnApi', () => { const { status: noteCreateStatus, record: noteRecord } = await dwnBob.records.create({ data, message: { - protocol : notesProtocolDefinition.protocol, + protocol : protocol.protocol, protocolPath : 'note', - schema : notesProtocolDefinition.types.note.schema, + schema : protocol.types.note.schema, dataFormat : 'text/plain', } }); @@ -2117,9 +2122,9 @@ describe('DwnApi', () => { data : 'friend!', message : { recipient : aliceDid.uri, - protocol : notesProtocolDefinition.protocol, + protocol : protocol.protocol, protocolPath : 'friend', - schema : notesProtocolDefinition.types.friend.schema, + schema : protocol.types.friend.schema, dataFormat : 'text/plain' } }); @@ -2133,7 +2138,7 @@ describe('DwnApi', () => { message : { protocolRole : 'friend', filter : { - protocol : notesProtocolDefinition.protocol, + protocol : protocol.protocol, protocolPath : 'note' } } @@ -2517,10 +2522,15 @@ describe('DwnApi', () => { }); it('ensures that a protocolRole used to subscribe is also used to read the data of the resulted records', async () => { + const protocol = { + ...notesProtocolDefinition, + protocol: 'http://example.com/notes' + TestDataGenerator.randomString(15) + }; + // Bob configures the notes protocol for himself const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ message: { - definition: notesProtocolDefinition + definition: protocol } }); expect(bobProtocolStatus.code).to.equal(202); @@ -2533,9 +2543,9 @@ describe('DwnApi', () => { data : 'friend!', message : { recipient : aliceDid.uri, - protocol : notesProtocolDefinition.protocol, + protocol : protocol.protocol, protocolPath : 'friend', - schema : notesProtocolDefinition.types.friend.schema, + schema : protocol.types.friend.schema, dataFormat : 'text/plain' } }); @@ -2555,7 +2565,7 @@ describe('DwnApi', () => { message : { protocolRole : 'friend', filter : { - protocol : notesProtocolDefinition.protocol, + protocol : protocol.protocol, protocolPath : 'note' } }, @@ -2572,9 +2582,9 @@ describe('DwnApi', () => { const { status: noteCreateStatus, record: noteRecord } = await dwnBob.records.create({ data, message: { - protocol : notesProtocolDefinition.protocol, + protocol : protocol.protocol, protocolPath : 'note', - schema : notesProtocolDefinition.types.note.schema, + schema : protocol.types.note.schema, dataFormat : 'text/plain', } }); diff --git a/packages/api/tests/fixtures/protocol-definitions/notes.json b/packages/api/tests/fixtures/protocol-definitions/notes.json index a98acf917..cdea6f33c 100644 --- a/packages/api/tests/fixtures/protocol-definitions/notes.json +++ b/packages/api/tests/fixtures/protocol-definitions/notes.json @@ -22,6 +22,13 @@ "text/plain", "application/json" ] + }, + "coAuthor" : { + "schema": "http://notes-protocol.xyz/schema/coAuthor", + "dataFormats": [ + "text/plain", + "application/json" + ] } }, "structure": { @@ -29,17 +36,27 @@ "$role": true }, "note": { + "coAuthor" : { + "$role": true + }, "$actions": [ { "role": "friend", "can": ["read", "query", "subscribe"] + }, + { + "role": "note/coAuthor", + "can": [ "co-update", "co-delete" ] } ], "comment": { "$actions": [ { "role": "friend", - "can": ["create", "delete", "read", "query", "subscribe"] + "can": ["create", "update", "delete", "read", "query", "subscribe"] + }, { + "role": "note/coAuthor", + "can": ["create", "update", "delete", "co-delete", "read", "query", "subscribe"] } ] } diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index 7d147d633..f4645f0eb 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -13,11 +13,12 @@ import { dataToBlob } from '../src/utils.js'; import { testDwnUrl } from './utils/test-config.js'; import { TestDataGenerator } from './utils/test-data-generator.js'; import emailProtocolDefinition from './fixtures/protocol-definitions/email.json' assert { type: 'json' }; +import notesProtocolDefinition from './fixtures/protocol-definitions/notes.json' assert { type: 'json' }; // NOTE: @noble/secp256k1 requires globalThis.crypto polyfill for node.js <=18: https://github.com/paulmillr/noble-secp256k1/blob/main/README.md#usage // Remove when we move off of node.js v18 to v20, earliest possible time would be Oct 2023: https://github.com/nodejs/release#release-schedule import { webcrypto } from 'node:crypto'; -import { Jws, Message, Poller } from '@tbd54566975/dwn-sdk-js'; +import { Jws, Message, Poller, RecordsWrite } from '@tbd54566975/dwn-sdk-js'; import { Web5 } from '../src/web5.js'; // @ts-ignore if (!globalThis.crypto) globalThis.crypto = webcrypto; @@ -3086,6 +3087,126 @@ describe('Record', () => { // bob is the author expect(readResultAlice.record!.author).to.equal(bobDid.uri); }); + + it('updates a record using a different protocolRole than the one used when querying for/reading the record', async () => { + const protocol = { + ...notesProtocolDefinition, + protocol: 'http://example.com/notes' + TestDataGenerator.randomString(15) + }; + + // Bob configures the notes protocol for himself + const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ + message: { + definition: protocol + } + }); + expect(bobProtocolStatus.code).to.equal(202); + const { status: bobProtocolSendStatus } = await bobProtocol.send(bobDid.uri); + expect(bobProtocolSendStatus.code).to.equal(202); + + // Alice must also configure the protocol to make updates. + // NOTE: This is not desireable and there is an issue to address this: + // https://github.com/TBD54566975/web5-js/issues/955 + const { status: aliceProtocolStatus, protocol: aliceProtocol } = await dwnAlice.protocols.configure({ + message: { + definition: protocol + } + }); + expect(aliceProtocolStatus.code).to.equal(202); + const { status: aliceProtocolSend } = await aliceProtocol.send(aliceDid.uri); + expect(aliceProtocolSend.code).to.equal(202); + + // Bob creates a few notes ensuring that the data is larger than the max encoded size + // that way the data will be requested with a separate `read` request + const records: Set = new Set(); + for (let i = 0; i < 3; i++) { + const data = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); + const { status: noteCreateStatus, record: noteRecord } = await dwnBob.records.create({ + data, + message: { + protocol : protocol.protocol, + protocolPath : 'note', + schema : protocol.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(noteCreateStatus.code).to.equal(202); + const { status: noteSendStatus } = await noteRecord.send(); + expect(noteSendStatus.code).to.equal(202); + records.add(noteRecord.id); + } + + // Bob makes Alice a `friend` to allow her to read and comment on his notes + const { status: friendCreateStatus, record: friendRecord} = await dwnBob.records.create({ + data : 'friend!', + message : { + recipient : aliceDid.uri, + protocol : protocol.protocol, + protocolPath : 'friend', + schema : protocol.types.friend.schema, + dataFormat : 'text/plain' + } + }); + expect(friendCreateStatus.code).to.equal(202); + const { status: bobFriendSendStatus } = await friendRecord.send(bobDid.uri); + expect(bobFriendSendStatus.code).to.equal(202); + + // Bob makes alice a 'coAuthor' of one of his notes + const aliceCoAuthorNoteId = records.keys().next().value; + const { status: coAuthorStatus, record: coAuthorRecord } = await dwnBob.records.create({ + data : aliceDid.uri, + message : { + parentContextId : aliceCoAuthorNoteId, + recipient : aliceDid.uri, + protocol : protocol.protocol, + protocolPath : 'note/coAuthor', + schema : protocol.types.coAuthor.schema, + dataFormat : 'text/plain' + } + }); + expect(coAuthorStatus.code).to.equal(202); + const { status: coAuthorSendStatus } = await coAuthorRecord.send(bobDid.uri); + expect(coAuthorSendStatus.code).to.equal(202); + + // Alice querying for bob's notes using her friend role + const { status: aliceQueryStatus, records: bobNotesAliceQuery } = await dwnAlice.records.query({ + from : bobDid.uri, + message : { + protocolRole : 'friend', + filter : { + protocol : protocol.protocol, + protocolPath : 'note', + } + } + }); + expect(aliceQueryStatus.code).to.equal(200); + expect(bobNotesAliceQuery).to.not.be.undefined; + expect(bobNotesAliceQuery.length).to.equal(records.size); + + // Alice looks for the record she has a co-author rule on + const coAuthorNote = bobNotesAliceQuery.find((record) => record.id === aliceCoAuthorNoteId); + expect(coAuthorNote).to.not.be.undefined; + + // Alice must import the record to be able to update it + // NOTE this should be removed after: https://github.com/TBD54566975/web5-js/issues/955 + const { status: importStatus } = await coAuthorNote.import(); + expect(importStatus.code).to.equal(202); + + // Alice updates the co-author note without providing a new role + const { status: updateStatus } = await coAuthorNote!.update({ data: 'updated note' }); + expect(updateStatus.code).to.equal(202); + + // This is accepted locally but will fail when sending the update to the remote DWN + const { status: sendStatus } = await coAuthorNote.send(bobDid.uri); + expect(sendStatus.code).to.equal(401); + + // Now update the record with the correct role + const { status: updateStatusCoAuthor } = await coAuthorNote!.update({ data: 'updated note', protocolRole: 'note/coAuthor' }); + expect(updateStatusCoAuthor.code).to.equal(202); + + const { status: sendStatusCoAuthor } = await coAuthorNote.send(bobDid.uri); + expect(sendStatusCoAuthor.code).to.equal(202); + }); }); describe('delete()', () => { @@ -3659,6 +3780,119 @@ describe('Record', () => { await subscription.close(); }); + + it('deletes a record using a different protocolRole than the one used when querying for/reading the record', async () => { + const protocol = { + ...notesProtocolDefinition, + protocol: 'http://example.com/notes' + TestDataGenerator.randomString(15) + }; + + // Bob configures the notes protocol for himself + const { status: bobProtocolStatus, protocol: bobProtocol } = await dwnBob.protocols.configure({ + message: { + definition: protocol + } + }); + expect(bobProtocolStatus.code).to.equal(202); + const { status: bobProtocolSendStatus } = await bobProtocol.send(bobDid.uri); + expect(bobProtocolSendStatus.code).to.equal(202); + + // Alice must also configure the protocol to make updates. + // NOTE: This is not desireable and there is an issue to address this: + // https://github.com/TBD54566975/web5-js/issues/955 + const { status: aliceProtocolStatus, protocol: aliceProtocol } = await dwnAlice.protocols.configure({ + message: { + definition: protocol + } + }); + expect(aliceProtocolStatus.code).to.equal(202); + const { status: aliceProtocolSend } = await aliceProtocol.send(aliceDid.uri); + expect(aliceProtocolSend.code).to.equal(202); + + // Bob creates a few notes ensuring that the data is larger than the max encoded size + // that way the data will be requested with a separate `read` request + const records: Set = new Set(); + for (let i = 0; i < 3; i++) { + const data = TestDataGenerator.randomString(DwnConstant.maxDataSizeAllowedToBeEncoded + 1); + const { status: noteCreateStatus, record: noteRecord } = await dwnBob.records.create({ + data, + message: { + protocol : protocol.protocol, + protocolPath : 'note', + schema : protocol.types.note.schema, + dataFormat : 'text/plain', + } + }); + expect(noteCreateStatus.code).to.equal(202); + const { status: noteSendStatus } = await noteRecord.send(); + expect(noteSendStatus.code).to.equal(202); + records.add(noteRecord.id); + } + + // Bob makes Alice a `friend` to allow her to read and comment on his notes + const { status: friendCreateStatus, record: friendRecord} = await dwnBob.records.create({ + data : 'friend!', + message : { + recipient : aliceDid.uri, + protocol : protocol.protocol, + protocolPath : 'friend', + schema : protocol.types.friend.schema, + dataFormat : 'text/plain' + } + }); + expect(friendCreateStatus.code).to.equal(202); + const { status: bobFriendSendStatus } = await friendRecord.send(bobDid.uri); + expect(bobFriendSendStatus.code).to.equal(202); + + // Bob makes alice a 'coAuthor' of one of his notes + const aliceCoAuthorNoteId = records.keys().next().value; + const { status: coAuthorStatus, record: coAuthorRecord } = await dwnBob.records.create({ + data : aliceDid.uri, + message : { + parentContextId : aliceCoAuthorNoteId, + recipient : aliceDid.uri, + protocol : protocol.protocol, + protocolPath : 'note/coAuthor', + schema : protocol.types.coAuthor.schema, + dataFormat : 'text/plain' + } + }); + expect(coAuthorStatus.code).to.equal(202); + const { status: coAuthorSendStatus } = await coAuthorRecord.send(bobDid.uri); + expect(coAuthorSendStatus.code).to.equal(202); + + // Alice querying for bob's notes using her friend role + const { status: aliceQueryStatus, records: bobNotesAliceQuery } = await dwnAlice.records.query({ + from : bobDid.uri, + message : { + protocolRole : 'friend', + filter : { + protocol : protocol.protocol, + protocolPath : 'note', + } + } + }); + expect(aliceQueryStatus.code).to.equal(200); + expect(bobNotesAliceQuery).to.not.be.undefined; + expect(bobNotesAliceQuery.length).to.equal(records.size); + + // Alice looks for the record she has a co-author rule on + const coDeleteNote = bobNotesAliceQuery.find((record) => record.id === aliceCoAuthorNoteId); + expect(coDeleteNote).to.not.be.undefined; + + const { status: deleteStatus } = await coDeleteNote.delete({ store: false }); + expect(deleteStatus.code).to.equal(202); + + const { status: sendDeleteStatus } = await coDeleteNote.send(bobDid.uri); + expect(sendDeleteStatus.code).to.equal(401); + + // Now update the record with the correct role + const { status: updateStatusCoAuthor } = await coDeleteNote.delete({ protocolRole: 'note/coAuthor', store: false }); + expect(updateStatusCoAuthor.code).to.equal(202, `delete: ${updateStatusCoAuthor.detail}`); + + const { status: sendStatusCoAuthor } = await coDeleteNote.send(bobDid.uri); + expect(sendStatusCoAuthor.code).to.equal(202, `delete send: ${sendStatusCoAuthor.detail}`); + }); }); describe('store()', () => { From e5d02af64ed5a89553d6835471efd057f582f8ba Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 18 Oct 2024 18:19:25 -0400 Subject: [PATCH 06/11] flesh out protocolRole and add helper method to extract role --- packages/agent/src/utils.ts | 10 +++++++++- packages/agent/tests/utils.spec.ts | 26 +++++++++++++++++++++++++- packages/api/src/dwn-api.ts | 2 +- packages/api/src/record.ts | 14 +++++++++----- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/agent/src/utils.ts b/packages/agent/src/utils.ts index 2ab38757f..a72c9f467 100644 --- a/packages/agent/src/utils.ts +++ b/packages/agent/src/utils.ts @@ -1,5 +1,5 @@ import type { DidUrlDereferencer } from '@web5/dids'; -import { PaginationCursor, RecordsDeleteMessage, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js'; +import { Jws, PaginationCursor, RecordsDeleteMessage, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js'; import { Readable } from '@web5/common'; import { utils as didUtils } from '@web5/dids'; @@ -42,6 +42,14 @@ export function getRecordAuthor(record: RecordsWriteMessage | RecordsDeleteMessa return Message.getAuthor(record); } +/** + * Get the `protocolRole` string from the signature payload of the given RecordsWriteMessage or RecordsDeleteMessage. + */ +export function getRecordProtocolRole(message: RecordsWriteMessage | RecordsDeleteMessage): string | undefined { + const signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature); + return signaturePayload?.protocolRole; +} + export function isRecordsWrite(obj: unknown): obj is RecordsWrite { // Validate that the given value is an object. if (!obj || typeof obj !== 'object' || obj === null) return false; diff --git a/packages/agent/tests/utils.spec.ts b/packages/agent/tests/utils.spec.ts index ebefcf424..f2c21f34d 100644 --- a/packages/agent/tests/utils.spec.ts +++ b/packages/agent/tests/utils.spec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { DateSort, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; -import { getPaginationCursor, getRecordAuthor, getRecordMessageCid } from '../src/utils.js'; +import { getPaginationCursor, getRecordAuthor, getRecordMessageCid, getRecordProtocolRole } from '../src/utils.js'; describe('Utils', () => { describe('getPaginationCursor', () => { @@ -84,4 +84,28 @@ describe('Utils', () => { expect(deleteAuthorFromFunction!).to.equal(recordsDeleteAuthor.did); }); }); + + describe('getRecordProtocolRole', () => { + it('gets a protocol role from a RecordsWrite', async () => { + const recordsWrite = await TestDataGenerator.generateRecordsWrite({ protocolRole: 'some-role' }); + const role = getRecordProtocolRole(recordsWrite.message); + expect(role).to.equal('some-role'); + }); + + it('gets a protocol role from a RecordsDelete', async () => { + const recordsDelete = await TestDataGenerator.generateRecordsDelete({ protocolRole: 'some-role' }); + const role = getRecordProtocolRole(recordsDelete.message); + expect(role).to.equal('some-role'); + }); + + it('returns undefined if no role is defined', async () => { + const recordsWrite = await TestDataGenerator.generateRecordsWrite(); + const writeRole = getRecordProtocolRole(recordsWrite.message); + expect(writeRole).to.be.undefined; + + const recordsDelete = await TestDataGenerator.generateRecordsDelete(); + const deleteRole = getRecordProtocolRole(recordsDelete.message); + expect(deleteRole).to.be.undefined; + }); + }); }); \ No newline at end of file diff --git a/packages/api/src/dwn-api.ts b/packages/api/src/dwn-api.ts index bcc481358..96d0e62a6 100644 --- a/packages/api/src/dwn-api.ts +++ b/packages/api/src/dwn-api.ts @@ -699,7 +699,7 @@ export class DwnApi { */ remoteOrigin : request.from, delegateDid : this.delegateDid, - protocolRole : request.message.protocolRole, + protocolRole : agentRequest.messageParams.protocolRole, ...entry as DwnMessage[DwnInterface.RecordsWrite] }; const record = new Record(this.agent, recordOptions, this.permissionsApi); diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index ffb5451d8..bef0c5f9c 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -21,6 +21,7 @@ import { SendDwnRequest, PermissionsApi, AgentPermissionsApi, + getRecordProtocolRole } from '@web5/agent'; import { Convert, isEmptyObject, NodeStream, removeUndefinedProperties, Stream } from '@web5/common'; @@ -150,7 +151,7 @@ export type RecordUpdateParams = { datePublished?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['datePublished']; /** The protocol role under which this record is written. */ - protocolRole?: string; + protocolRole?: RecordOptions['protocolRole']; /** The published status of the record. */ published?: DwnMessageDescriptor[DwnInterface.RecordsWrite]['published']; @@ -224,8 +225,6 @@ export class Record implements RecordModel { private _readableStream?: Readable; /** The origin DID if the record was fetched from a remote DWN. */ private _remoteOrigin?: string; - /** The protocolRole to use when reading the record */ - private _protocolRole?: string; // Private variables for DWN `RecordsWrite` message properties. @@ -251,6 +250,8 @@ export class Record implements RecordModel { private _initialWriteSigned: boolean; /** Unique identifier of the record. */ private _recordId: string; + /** Role under which the record is written. */ + private _protocolRole?: RecordOptions['protocolRole']; /** The `RecordsWriteMessage` descriptor unless the record is in a deleted state */ private get _recordsWriteDescriptor() { @@ -330,6 +331,9 @@ export class Record implements RecordModel { /** Record's signatures attestation */ get attestation(): DwnMessage[DwnInterface.RecordsWrite]['attestation'] | undefined { return this._attestation; } + /** Role under which the author is writing the record */ + get protocolRole() { return this._protocolRole; } + /** Record's deleted state (true/false) */ get deleted() { return isDwnMessage(DwnInterface.RecordsDelete, this.rawMessage); } @@ -782,6 +786,7 @@ export class Record implements RecordModel { // Only update the local Record instance mutable properties if the record was successfully (over)written. this._authorization = responseMessage.authorization; + this._protocolRole = updateMessage.protocolRole; mutableDescriptorProperties.forEach(property => { this._descriptor[property] = responseMessage.descriptor[property]; }); @@ -831,8 +836,7 @@ export class Record implements RecordModel { // Check to see if the provided protocolRole is different from the current protocolRole // If so we need to construct a delete message with the new protocolRole, otherwise we can use the existing - // NOTE: currently this is testing the instance _protocolRole, not the actual signature payload. - const differentRole = deleteParams?.protocolRole ? this._protocolRole !== deleteParams.protocolRole : false; + const differentRole = deleteParams?.protocolRole ? getRecordProtocolRole(this.rawMessage) !== deleteParams.protocolRole : false; if (this.deleted && !differentRole) { // if we have a delete message we can just use it deleteOptions.rawMessage = this.rawMessage as DwnMessage[DwnInterface.RecordsDelete]; From 037b6b74018a0d9066b3507457865a1c4b9d8af4 Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 18 Oct 2024 18:25:21 -0400 Subject: [PATCH 07/11] update comments/refactor --- packages/api/src/record.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index bef0c5f9c..2bce08c99 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -62,6 +62,9 @@ export type RecordModel = ImmutableRecordProperties & OptionalRecordProperties & /** The timestamp indicating when the record was last modified. */ messageTimestamp?: string; + + /** The protocol role under which this record is written. */ + protocolRole?: RecordOptions['protocolRole']; } /** @@ -651,6 +654,7 @@ export class Record implements RecordModel { parentId : this.parentId, protocol : this.protocol, protocolPath : this.protocolPath, + protocolRole : this.protocolRole, published : this.published, recipient : this.recipient, recordId : this.id, @@ -834,11 +838,11 @@ export class Record implements RecordModel { store }; - // Check to see if the provided protocolRole is different from the current protocolRole - // If so we need to construct a delete message with the new protocolRole, otherwise we can use the existing + // Check to see if the provided protocolRole within the deleteParams is different from the current protocolRole. const differentRole = deleteParams?.protocolRole ? getRecordProtocolRole(this.rawMessage) !== deleteParams.protocolRole : false; + // If the record is already in a deleted state but the protocolRole is different, we need to construct a delete message with the new protocolRole + // otherwise we can just use the existing delete message. if (this.deleted && !differentRole) { - // if we have a delete message we can just use it deleteOptions.rawMessage = this.rawMessage as DwnMessage[DwnInterface.RecordsDelete]; } else { // otherwise we construct a delete message given the `RecordDeleteParams` From 0dcfea9fe022b4fc61e2d1d835ab9e9dc5ff521d Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 18 Oct 2024 19:45:37 -0400 Subject: [PATCH 08/11] properly test expected behavior not only expeted results, add commens to code --- packages/api/src/record.ts | 4 +-- packages/api/tests/dwn-api.spec.ts | 48 +++++++++++++++++++++++++----- packages/api/tests/record.spec.ts | 46 ++++++++++++++++++++++++++-- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/packages/api/src/record.ts b/packages/api/src/record.ts index 2bce08c99..e93b2463f 100644 --- a/packages/api/src/record.ts +++ b/packages/api/src/record.ts @@ -721,7 +721,7 @@ export class Record implements RecordModel { ...descriptor, ...params, parentContextId, - protocolRole, + protocolRole : protocolRole ?? this._protocolRole, // Use the current protocolRole if not provided. messageTimestamp : dateModified, // Map Record class `dateModified` property to DWN SDK `messageTimestamp` recordId : this._recordId }; @@ -850,7 +850,7 @@ export class Record implements RecordModel { prune : prune, recordId : this._recordId, messageTimestamp : dateModified, - protocolRole : deleteParams?.protocolRole + protocolRole : deleteParams?.protocolRole ?? this._protocolRole // if no protocolRole is provided, use the current protocolRole }; } diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 7ee55cc0e..992d7368b 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -3,7 +3,7 @@ import type { BearerDid } from '@web5/dids'; import sinon from 'sinon'; import { expect } from 'chai'; import { Web5UserAgent } from '@web5/user-agent'; -import { AgentPermissionsApi, DwnDateSort, DwnProtocolDefinition, getRecordAuthor, Oidc, PlatformAgentTestHarness, WalletConnect } from '@web5/agent'; +import { AgentPermissionsApi, DwnDateSort, DwnInterface, DwnProtocolDefinition, getRecordAuthor, Oidc, PlatformAgentTestHarness, ProcessDwnRequest, SendDwnRequest, WalletConnect } from '@web5/agent'; import { DwnApi } from '../src/dwn-api.js'; import { testDwnUrl } from './utils/test-config.js'; @@ -2082,6 +2082,10 @@ describe('DwnApi', () => { }); it('ensures that a protocolRole used to query is also used to read the data of the resulted records', async () => { + // scenario: Bob has a protocol where he can write notes and add friends who can query and read these notes + // Alice is a friend of Bob and she queries for the notes and reads the data of the notes + // the protocolRole used to query for the notes should also be used to read the data of the notes + const protocol = { ...notesProtocolDefinition, protocol: 'http://example.com/notes' + TestDataGenerator.randomString(15) @@ -2147,12 +2151,26 @@ describe('DwnApi', () => { expect(noteRecords).to.exist; expect(noteRecords).to.have.lengthOf(3); + // spy on sendDwnRequest to ensure that the protocolRole is used to read the data of the notes + const sendDwnRequestSpy = sinon.spy(testHarness.agent, 'sendDwnRequest'); + + // confirm that it starts with 0 calls + expect(sendDwnRequestSpy.callCount).to.equal(0); // Alice attempts to read the data of the notes, which should succeed for (const record of noteRecords) { const readResult = await record.data.text(); const expectedData = recordData.get(record.id); expect(readResult).to.equal(expectedData); } + + // confirm that it was called 3 times + expect(sendDwnRequestSpy.callCount).to.equal(3); + + // confirm that the protocolRole was used to read the data of the notes + expect(sendDwnRequestSpy.getCalls().every(call => + call.args[0].messageType === DwnInterface.RecordsRead && + (call.args[0] as ProcessDwnRequest).messageParams.protocolRole === 'friend' + )).to.be.true; }); }); }); @@ -2522,6 +2540,9 @@ describe('DwnApi', () => { }); it('ensures that a protocolRole used to subscribe is also used to read the data of the resulted records', async () => { + // scenario: Bob has a protocol where he can write notes and add friends who can subscribe and read these notes + // When Alice subscribes to the notes protocol using the role, the role should also be used to read the data of the notes + const protocol = { ...notesProtocolDefinition, protocol: 'http://example.com/notes' + TestDataGenerator.randomString(15) @@ -2555,11 +2576,6 @@ describe('DwnApi', () => { // Alice subscribes to the notes protocol using the role const notes: Map = new Map(); - const subscriptionHandler = async (record: Record) => { - notes.set(record.id, record); - }; - - // alice uses the role to query for the available notes const { status: notesSubscribeStatus, subscription } = await dwnAlice.records.subscribe({ from : bobDid.uri, message : { @@ -2569,7 +2585,10 @@ describe('DwnApi', () => { protocolPath : 'note' } }, - subscriptionHandler + subscriptionHandler: (record) => { + // add to the notes map + notes.set(record.id, record); + } }); expect(notesSubscribeStatus.code).to.equal(200); expect(subscription).to.exist; @@ -2599,11 +2618,26 @@ describe('DwnApi', () => { expect(notes.size).to.equal(3); }); + // spy on sendDwnRequest to ensure that the protocolRole is used to read the data of the notes + const sendDwnRequestSpy = sinon.spy(testHarness.agent, 'sendDwnRequest'); + + // confirm that it starts with 0 calls + expect(sendDwnRequestSpy.callCount).to.equal(0); + // Alice attempts to read the data of the notes, which should succeed for (const record of notes.values()) { const readResult = await record.data.text(); const expectedData = recordData.get(record.id); expect(readResult).to.equal(expectedData); } + + // confirm that it was called 3 times + expect(sendDwnRequestSpy.callCount).to.equal(3); + + // confirm that the protocolRole was used to read the data of the notes + expect(sendDwnRequestSpy.getCalls().every(call => + call.args[0].messageType === DwnInterface.RecordsRead && + (call.args[0] as ProcessDwnRequest).messageParams.protocolRole === 'friend' + )).to.be.true; }); }); }); diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index f4645f0eb..136ca258e 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -1,12 +1,12 @@ import type { BearerDid ,PortableDid } from '@web5/dids'; -import type { DwnMessageParams, DwnProtocolDefinition, DwnPublicKeyJwk, DwnSigner } from '@web5/agent'; +import type { DwnMessageParams, DwnProtocolDefinition, DwnPublicKeyJwk, DwnSigner, ProcessDwnRequest, SendDwnRequest } from '@web5/agent'; import sinon from 'sinon'; import { expect } from 'chai'; import { NodeStream } from '@web5/common'; import { utils as didUtils } from '@web5/dids'; import { Web5UserAgent } from '@web5/user-agent'; -import { DwnConstant, DwnDateSort, DwnEncryptionAlgorithm, DwnInterface, DwnKeyDerivationScheme, dwnMessageConstructors, getRecordAuthor, Oidc, PlatformAgentTestHarness, WalletConnect } from '@web5/agent'; +import { DwnConstant, DwnDateSort, DwnEncryptionAlgorithm, DwnInterface, DwnKeyDerivationScheme, dwnMessageConstructors, getRecordAuthor, getRecordProtocolRole, Oidc, PlatformAgentTestHarness, WalletConnect } from '@web5/agent'; import { Record } from '../src/record.js'; import { DwnApi } from '../src/dwn-api.js'; import { dataToBlob } from '../src/utils.js'; @@ -3089,6 +3089,9 @@ describe('Record', () => { }); it('updates a record using a different protocolRole than the one used when querying for/reading the record', async () => { + // scenario: Bob has a notes protocol that has friends who can read/query/subscribe to notes, but coAuthors that can update notes. + // When Alice uses her friend role to query for notes, she cannot update them with that same role. Instead she uses her coAuthor role update. + const protocol = { ...notesProtocolDefinition, protocol: 'http://example.com/notes' + TestDataGenerator.randomString(15) @@ -3196,16 +3199,32 @@ describe('Record', () => { const { status: updateStatus } = await coAuthorNote!.update({ data: 'updated note' }); expect(updateStatus.code).to.equal(202); + // spy on sendDwnRequest to ensure that the protocolRole is used to read the data of the notes + const sendDwnRequestSpy = sinon.spy(testHarness.agent, 'sendDwnRequest'); + + // confirm that it starts with 0 calls + expect(sendDwnRequestSpy.callCount).to.equal(0); + // This is accepted locally but will fail when sending the update to the remote DWN const { status: sendStatus } = await coAuthorNote.send(bobDid.uri); expect(sendStatus.code).to.equal(401); + expect(sendDwnRequestSpy.callCount).to.equal(2); // the first call is for the initialWrite + let record = (sendDwnRequestSpy.secondCall.args[0] as ProcessDwnRequest).rawMessage; + let sendAuthorizationRole = getRecordProtocolRole(record); + expect(sendAuthorizationRole).to.equal('friend'); - // Now update the record with the correct role const { status: updateStatusCoAuthor } = await coAuthorNote!.update({ data: 'updated note', protocolRole: 'note/coAuthor' }); expect(updateStatusCoAuthor.code).to.equal(202); + sendDwnRequestSpy.resetHistory(); + + // Now update the record with the correct role const { status: sendStatusCoAuthor } = await coAuthorNote.send(bobDid.uri); expect(sendStatusCoAuthor.code).to.equal(202); + expect(sendDwnRequestSpy.callCount).to.equal(1); // the initialWrite was already sent and added to the sent-cache, only the update is sent + record = (sendDwnRequestSpy.firstCall.args[0] as ProcessDwnRequest).rawMessage; + sendAuthorizationRole = getRecordProtocolRole(record); + expect(sendAuthorizationRole).to.equal('note/coAuthor'); }); }); @@ -3782,6 +3801,9 @@ describe('Record', () => { }); it('deletes a record using a different protocolRole than the one used when querying for/reading the record', async () => { + // scenario: Bob has a notes protocol that has friends who can read/query/subscribe to notes, but coAuthors that can update/delete notes. + // When Alice uses her friend role to query for notes, she cannot delete them with that same role. Instead she uses her coAuthor role to delete. + const protocol = { ...notesProtocolDefinition, protocol: 'http://example.com/notes' + TestDataGenerator.randomString(15) @@ -3880,18 +3902,36 @@ describe('Record', () => { const coDeleteNote = bobNotesAliceQuery.find((record) => record.id === aliceCoAuthorNoteId); expect(coDeleteNote).to.not.be.undefined; + // spy on sendDwnRequest to ensure that the protocolRole is used to read the data of the notes + const sendDwnRequestSpy = sinon.spy(testHarness.agent, 'sendDwnRequest'); + + // confirm that it starts with 0 calls + expect(sendDwnRequestSpy.callCount).to.equal(0); + const { status: deleteStatus } = await coDeleteNote.delete({ store: false }); expect(deleteStatus.code).to.equal(202); const { status: sendDeleteStatus } = await coDeleteNote.send(bobDid.uri); expect(sendDeleteStatus.code).to.equal(401); + expect(sendDwnRequestSpy.callCount).to.equal(2); // the first call is for the initialWrite + let record = (sendDwnRequestSpy.secondCall.args[0] as ProcessDwnRequest).rawMessage; + let sendAuthorizationRole = getRecordProtocolRole(record); + expect(sendAuthorizationRole).to.equal('friend'); + + sendDwnRequestSpy.resetHistory(); + // Now update the record with the correct role const { status: updateStatusCoAuthor } = await coDeleteNote.delete({ protocolRole: 'note/coAuthor', store: false }); expect(updateStatusCoAuthor.code).to.equal(202, `delete: ${updateStatusCoAuthor.detail}`); const { status: sendStatusCoAuthor } = await coDeleteNote.send(bobDid.uri); expect(sendStatusCoAuthor.code).to.equal(202, `delete send: ${sendStatusCoAuthor.detail}`); + + expect(sendDwnRequestSpy.callCount).to.equal(1); // the initialWrite was already sent and added to the sent-cache, only the update is sent + record = (sendDwnRequestSpy.firstCall.args[0] as ProcessDwnRequest).rawMessage; + sendAuthorizationRole = getRecordProtocolRole(record); + expect(sendAuthorizationRole).to.equal('note/coAuthor'); }); }); From 1715a8cc8284c429ccf0cdf2799a28c3bc81024c Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 18 Oct 2024 19:47:32 -0400 Subject: [PATCH 09/11] unused imports --- packages/api/tests/dwn-api.spec.ts | 2 +- packages/api/tests/record.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/tests/dwn-api.spec.ts b/packages/api/tests/dwn-api.spec.ts index 992d7368b..e2fbb3730 100644 --- a/packages/api/tests/dwn-api.spec.ts +++ b/packages/api/tests/dwn-api.spec.ts @@ -3,7 +3,7 @@ import type { BearerDid } from '@web5/dids'; import sinon from 'sinon'; import { expect } from 'chai'; import { Web5UserAgent } from '@web5/user-agent'; -import { AgentPermissionsApi, DwnDateSort, DwnInterface, DwnProtocolDefinition, getRecordAuthor, Oidc, PlatformAgentTestHarness, ProcessDwnRequest, SendDwnRequest, WalletConnect } from '@web5/agent'; +import { AgentPermissionsApi, DwnDateSort, DwnInterface, DwnProtocolDefinition, getRecordAuthor, Oidc, PlatformAgentTestHarness, ProcessDwnRequest, WalletConnect } from '@web5/agent'; import { DwnApi } from '../src/dwn-api.js'; import { testDwnUrl } from './utils/test-config.js'; diff --git a/packages/api/tests/record.spec.ts b/packages/api/tests/record.spec.ts index 136ca258e..8a14b5bb0 100644 --- a/packages/api/tests/record.spec.ts +++ b/packages/api/tests/record.spec.ts @@ -1,5 +1,5 @@ import type { BearerDid ,PortableDid } from '@web5/dids'; -import type { DwnMessageParams, DwnProtocolDefinition, DwnPublicKeyJwk, DwnSigner, ProcessDwnRequest, SendDwnRequest } from '@web5/agent'; +import type { DwnMessageParams, DwnProtocolDefinition, DwnPublicKeyJwk, DwnSigner, ProcessDwnRequest } from '@web5/agent'; import sinon from 'sinon'; import { expect } from 'chai'; From c5b94c278ab25964dc1ea448048005ecf957319c Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Fri, 18 Oct 2024 19:53:14 -0400 Subject: [PATCH 10/11] complete coverage --- packages/agent/tests/utils.spec.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/agent/tests/utils.spec.ts b/packages/agent/tests/utils.spec.ts index f2c21f34d..3751613e8 100644 --- a/packages/agent/tests/utils.spec.ts +++ b/packages/agent/tests/utils.spec.ts @@ -1,9 +1,18 @@ import { expect } from 'chai'; +import sinon from 'sinon'; -import { DateSort, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; +import { DateSort, Jws, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; import { getPaginationCursor, getRecordAuthor, getRecordMessageCid, getRecordProtocolRole } from '../src/utils.js'; describe('Utils', () => { + beforeEach(() => { + sinon.restore(); + }); + + after(() => { + sinon.restore(); + }); + describe('getPaginationCursor', () => { it('should return a PaginationCursor object', async () => { // create a RecordWriteMessage object which is published @@ -107,5 +116,12 @@ describe('Utils', () => { const deleteRole = getRecordProtocolRole(recordsDelete.message); expect(deleteRole).to.be.undefined; }); + + it('returns undefined if decodedObject is undefined', async () => { + sinon.stub(Jws, 'decodePlainObjectPayload').returns(undefined); + const recordsWrite = await TestDataGenerator.generateRecordsWrite(); + const writeRole = getRecordProtocolRole(recordsWrite.message); + expect(writeRole).to.be.undefined; + }); }); }); \ No newline at end of file From ec3b2c557dcc783bc735e7b623bfb1a07b6ed5fd Mon Sep 17 00:00:00 2001 From: Liran Cohen Date: Mon, 21 Oct 2024 09:44:08 -0400 Subject: [PATCH 11/11] add changeset --- .changeset/many-suns-think.md | 8 ++++++++ .changeset/slimy-mayflies-hide.md | 5 +++++ 2 files changed, 13 insertions(+) create mode 100644 .changeset/many-suns-think.md create mode 100644 .changeset/slimy-mayflies-hide.md diff --git a/.changeset/many-suns-think.md b/.changeset/many-suns-think.md new file mode 100644 index 000000000..02246e9b7 --- /dev/null +++ b/.changeset/many-suns-think.md @@ -0,0 +1,8 @@ +--- +"@web5/agent": patch +"@web5/identity-agent": patch +"@web5/proxy-agent": patch +"@web5/user-agent": patch +--- + +Add `getProtocolRole` util diff --git a/.changeset/slimy-mayflies-hide.md b/.changeset/slimy-mayflies-hide.md new file mode 100644 index 000000000..64357adce --- /dev/null +++ b/.changeset/slimy-mayflies-hide.md @@ -0,0 +1,5 @@ +--- +"@web5/api": patch +--- + +Ensure protocolRole is maintained between query/read and subscribe/read.