Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure protocolRole is maintained between query/read and subscribe/read. #954

Merged
merged 11 commits into from
Oct 21, 2024
8 changes: 8 additions & 0 deletions .changeset/many-suns-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@web5/agent": patch
"@web5/identity-agent": patch
"@web5/proxy-agent": patch
"@web5/user-agent": patch
---

Add `getProtocolRole` util
5 changes: 5 additions & 0 deletions .changeset/slimy-mayflies-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@web5/api": patch
---

Ensure protocolRole is maintained between query/read and subscribe/read.
10 changes: 9 additions & 1 deletion packages/agent/src/utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down
44 changes: 42 additions & 2 deletions packages/agent/tests/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { expect } from 'chai';
import sinon from 'sinon';

import { DateSort, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js';
import { getPaginationCursor, getRecordAuthor, getRecordMessageCid } from '../src/utils.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
Expand Down Expand Up @@ -84,4 +93,35 @@ 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;
});

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;
});
});
});
2 changes: 2 additions & 0 deletions packages/api/src/dwn-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,7 @@ export class DwnApi {
*/
remoteOrigin : request.from,
delegateDid : this.delegateDid,
protocolRole : agentRequest.messageParams.protocolRole,
...entry as DwnMessage[DwnInterface.RecordsWrite]
};
const record = new Record(this.agent, recordOptions, this.permissionsApi);
Expand Down Expand Up @@ -829,6 +830,7 @@ export class DwnApi {
connectedDid : this.connectedDid,
delegateDid : this.delegateDid,
permissionsApi : this.permissionsApi,
protocolRole : request.message.protocolRole,
request
})
};
Expand Down
20 changes: 14 additions & 6 deletions packages/api/src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
SendDwnRequest,
PermissionsApi,
AgentPermissionsApi,
getRecordProtocolRole
} from '@web5/agent';

import { Convert, isEmptyObject, NodeStream, removeUndefinedProperties, Stream } from '@web5/common';
Expand Down Expand Up @@ -183,6 +184,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?: string;
};

/**
Expand Down Expand Up @@ -311,7 +315,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; }
Expand Down Expand Up @@ -703,7 +706,7 @@ export class Record implements RecordModel {
*
* @beta
*/
async update({ dateModified, data, ...params }: RecordUpdateParams): Promise<DwnResponseStatus> {
async update({ dateModified, data, protocolRole, ...params }: RecordUpdateParams): Promise<DwnResponseStatus> {

if (this.deleted) {
throw new Error('Record: Cannot revive a deleted record.');
Expand All @@ -718,6 +721,7 @@ export class Record implements RecordModel {
...descriptor,
...params,
parentContextId,
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
};
Expand Down Expand Up @@ -786,7 +790,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 = params.protocolRole;
this._protocolRole = updateMessage.protocolRole;
mutableDescriptorProperties.forEach(property => {
this._descriptor[property] = responseMessage.descriptor[property];
});
Expand Down Expand Up @@ -834,15 +838,19 @@ export class Record implements RecordModel {
store
};

if (this.deleted) {
// if we have a delete message we can just use it
// 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) {
deleteOptions.rawMessage = this.rawMessage as DwnMessage[DwnInterface.RecordsDelete];
} else {
// otherwise we construct a delete message given the `RecordDeleteParams`
deleteOptions.messageParams = {
prune : prune,
recordId : this._recordId,
messageTimestamp : dateModified,
protocolRole : deleteParams?.protocolRole ?? this._protocolRole // if no protocolRole is provided, use the current protocolRole
};
}

Expand Down Expand Up @@ -1023,7 +1031,7 @@ export class Record implements RecordModel {
private async readRecordData({ target, isRemote }: { target: string, isRemote: boolean }) {
const readRequest: ProcessDwnRequest<DwnInterface.RecordsRead> = {
author : this._connectedDid,
messageParams : { filter: { recordId: this.id } },
messageParams : { filter: { recordId: this.id }, protocolRole: this._protocolRole },
messageType : DwnInterface.RecordsRead,
target,
};
Expand Down
4 changes: 3 additions & 1 deletion packages/api/src/subscription-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -31,6 +32,7 @@ export class SubscriptionUtil {
const record = new Record(agent, {
...message,
...recordOptions,
protocolRole,
delegateDid: delegateDid,
}, permissionsApi);

Expand Down
Loading
Loading