Skip to content

Commit 5120f6f

Browse files
authored
Ensure protocolRole is maintained between query/read and subscribe/read. (#954)
Before this PR that were some inconsistencies with using `protocolRole`. There were instances where a user would query using a role but not be able to read the data of the given record because the role was not being applied. Same would happen during update/delete. This PR allows the READ operation to inherit the `protocolRole` used for a `query` or `subscribe` if it exists. Additionally it provides the user the ability to provide a different role when performing an `update` or `delete` operation.
1 parent 3f39bf1 commit 5120f6f

File tree

10 files changed

+621
-15
lines changed

10 files changed

+621
-15
lines changed

.changeset/many-suns-think.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@web5/agent": patch
3+
"@web5/identity-agent": patch
4+
"@web5/proxy-agent": patch
5+
"@web5/user-agent": patch
6+
---
7+
8+
Add `getProtocolRole` util

.changeset/slimy-mayflies-hide.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@web5/api": patch
3+
---
4+
5+
Ensure protocolRole is maintained between query/read and subscribe/read.

packages/agent/src/utils.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DidUrlDereferencer } from '@web5/dids';
2-
import { PaginationCursor, RecordsDeleteMessage, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js';
2+
import { Jws, PaginationCursor, RecordsDeleteMessage, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js';
33

44
import { Readable } from '@web5/common';
55
import { utils as didUtils } from '@web5/dids';
@@ -42,6 +42,14 @@ export function getRecordAuthor(record: RecordsWriteMessage | RecordsDeleteMessa
4242
return Message.getAuthor(record);
4343
}
4444

45+
/**
46+
* Get the `protocolRole` string from the signature payload of the given RecordsWriteMessage or RecordsDeleteMessage.
47+
*/
48+
export function getRecordProtocolRole(message: RecordsWriteMessage | RecordsDeleteMessage): string | undefined {
49+
const signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature);
50+
return signaturePayload?.protocolRole;
51+
}
52+
4553
export function isRecordsWrite(obj: unknown): obj is RecordsWrite {
4654
// Validate that the given value is an object.
4755
if (!obj || typeof obj !== 'object' || obj === null) return false;

packages/agent/tests/utils.spec.ts

+42-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import { expect } from 'chai';
2+
import sinon from 'sinon';
23

3-
import { DateSort, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js';
4-
import { getPaginationCursor, getRecordAuthor, getRecordMessageCid } from '../src/utils.js';
4+
import { DateSort, Jws, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js';
5+
import { getPaginationCursor, getRecordAuthor, getRecordMessageCid, getRecordProtocolRole } from '../src/utils.js';
56

67
describe('Utils', () => {
8+
beforeEach(() => {
9+
sinon.restore();
10+
});
11+
12+
after(() => {
13+
sinon.restore();
14+
});
15+
716
describe('getPaginationCursor', () => {
817
it('should return a PaginationCursor object', async () => {
918
// create a RecordWriteMessage object which is published
@@ -84,4 +93,35 @@ describe('Utils', () => {
8493
expect(deleteAuthorFromFunction!).to.equal(recordsDeleteAuthor.did);
8594
});
8695
});
96+
97+
describe('getRecordProtocolRole', () => {
98+
it('gets a protocol role from a RecordsWrite', async () => {
99+
const recordsWrite = await TestDataGenerator.generateRecordsWrite({ protocolRole: 'some-role' });
100+
const role = getRecordProtocolRole(recordsWrite.message);
101+
expect(role).to.equal('some-role');
102+
});
103+
104+
it('gets a protocol role from a RecordsDelete', async () => {
105+
const recordsDelete = await TestDataGenerator.generateRecordsDelete({ protocolRole: 'some-role' });
106+
const role = getRecordProtocolRole(recordsDelete.message);
107+
expect(role).to.equal('some-role');
108+
});
109+
110+
it('returns undefined if no role is defined', async () => {
111+
const recordsWrite = await TestDataGenerator.generateRecordsWrite();
112+
const writeRole = getRecordProtocolRole(recordsWrite.message);
113+
expect(writeRole).to.be.undefined;
114+
115+
const recordsDelete = await TestDataGenerator.generateRecordsDelete();
116+
const deleteRole = getRecordProtocolRole(recordsDelete.message);
117+
expect(deleteRole).to.be.undefined;
118+
});
119+
120+
it('returns undefined if decodedObject is undefined', async () => {
121+
sinon.stub(Jws, 'decodePlainObjectPayload').returns(undefined);
122+
const recordsWrite = await TestDataGenerator.generateRecordsWrite();
123+
const writeRole = getRecordProtocolRole(recordsWrite.message);
124+
expect(writeRole).to.be.undefined;
125+
});
126+
});
87127
});

packages/api/src/dwn-api.ts

+2
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ export class DwnApi {
699699
*/
700700
remoteOrigin : request.from,
701701
delegateDid : this.delegateDid,
702+
protocolRole : agentRequest.messageParams.protocolRole,
702703
...entry as DwnMessage[DwnInterface.RecordsWrite]
703704
};
704705
const record = new Record(this.agent, recordOptions, this.permissionsApi);
@@ -829,6 +830,7 @@ export class DwnApi {
829830
connectedDid : this.connectedDid,
830831
delegateDid : this.delegateDid,
831832
permissionsApi : this.permissionsApi,
833+
protocolRole : request.message.protocolRole,
832834
request
833835
})
834836
};

packages/api/src/record.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
SendDwnRequest,
2222
PermissionsApi,
2323
AgentPermissionsApi,
24+
getRecordProtocolRole
2425
} from '@web5/agent';
2526

2627
import { Convert, isEmptyObject, NodeStream, removeUndefinedProperties, Stream } from '@web5/common';
@@ -183,6 +184,9 @@ export type RecordDeleteParams = {
183184

184185
/** The timestamp indicating when the record was deleted. */
185186
dateModified?: DwnMessageDescriptor[DwnInterface.RecordsDelete]['messageTimestamp'];
187+
188+
/** The protocol role under which this record will be deleted. */
189+
protocolRole?: string;
186190
};
187191

188192
/**
@@ -311,7 +315,6 @@ export class Record implements RecordModel {
311315
/** Tags of the record */
312316
get tags() { return this._recordsWriteDescriptor?.tags; }
313317

314-
315318
// Getters for for properties that depend on the current state of the Record.
316319
/** DID that is the logical author of the Record. */
317320
get author(): string { return this._author; }
@@ -703,7 +706,7 @@ export class Record implements RecordModel {
703706
*
704707
* @beta
705708
*/
706-
async update({ dateModified, data, ...params }: RecordUpdateParams): Promise<DwnResponseStatus> {
709+
async update({ dateModified, data, protocolRole, ...params }: RecordUpdateParams): Promise<DwnResponseStatus> {
707710

708711
if (this.deleted) {
709712
throw new Error('Record: Cannot revive a deleted record.');
@@ -718,6 +721,7 @@ export class Record implements RecordModel {
718721
...descriptor,
719722
...params,
720723
parentContextId,
724+
protocolRole : protocolRole ?? this._protocolRole, // Use the current protocolRole if not provided.
721725
messageTimestamp : dateModified, // Map Record class `dateModified` property to DWN SDK `messageTimestamp`
722726
recordId : this._recordId
723727
};
@@ -786,7 +790,7 @@ export class Record implements RecordModel {
786790

787791
// Only update the local Record instance mutable properties if the record was successfully (over)written.
788792
this._authorization = responseMessage.authorization;
789-
this._protocolRole = params.protocolRole;
793+
this._protocolRole = updateMessage.protocolRole;
790794
mutableDescriptorProperties.forEach(property => {
791795
this._descriptor[property] = responseMessage.descriptor[property];
792796
});
@@ -834,15 +838,19 @@ export class Record implements RecordModel {
834838
store
835839
};
836840

837-
if (this.deleted) {
838-
// if we have a delete message we can just use it
841+
// Check to see if the provided protocolRole within the deleteParams is different from the current protocolRole.
842+
const differentRole = deleteParams?.protocolRole ? getRecordProtocolRole(this.rawMessage) !== deleteParams.protocolRole : false;
843+
// 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
844+
// otherwise we can just use the existing delete message.
845+
if (this.deleted && !differentRole) {
839846
deleteOptions.rawMessage = this.rawMessage as DwnMessage[DwnInterface.RecordsDelete];
840847
} else {
841848
// otherwise we construct a delete message given the `RecordDeleteParams`
842849
deleteOptions.messageParams = {
843850
prune : prune,
844851
recordId : this._recordId,
845852
messageTimestamp : dateModified,
853+
protocolRole : deleteParams?.protocolRole ?? this._protocolRole // if no protocolRole is provided, use the current protocolRole
846854
};
847855
}
848856

@@ -1023,7 +1031,7 @@ export class Record implements RecordModel {
10231031
private async readRecordData({ target, isRemote }: { target: string, isRemote: boolean }) {
10241032
const readRequest: ProcessDwnRequest<DwnInterface.RecordsRead> = {
10251033
author : this._connectedDid,
1026-
messageParams : { filter: { recordId: this.id } },
1034+
messageParams : { filter: { recordId: this.id }, protocolRole: this._protocolRole },
10271035
messageType : DwnInterface.RecordsRead,
10281036
target,
10291037
};

packages/api/src/subscription-util.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ export class SubscriptionUtil {
99
/**
1010
* Creates a record subscription handler that can be used to process incoming {Record} messages.
1111
*/
12-
static recordSubscriptionHandler({ agent, connectedDid, request, delegateDid, permissionsApi }:{
12+
static recordSubscriptionHandler({ agent, connectedDid, request, delegateDid, protocolRole, permissionsApi }:{
1313
agent: Web5Agent;
1414
connectedDid: string;
1515
delegateDid?: string;
16+
protocolRole?: string;
1617
permissionsApi?: PermissionsApi;
1718
request: RecordsSubscribeRequest;
1819
}): DwnRecordSubscriptionHandler {
@@ -31,6 +32,7 @@ export class SubscriptionUtil {
3132
const record = new Record(agent, {
3233
...message,
3334
...recordOptions,
35+
protocolRole,
3436
delegateDid: delegateDid,
3537
}, permissionsApi);
3638

0 commit comments

Comments
 (0)