diff --git a/.changeset/fresh-olives-dance.md b/.changeset/fresh-olives-dance.md
new file mode 100644
index 000000000..e611f3362
--- /dev/null
+++ b/.changeset/fresh-olives-dance.md
@@ -0,0 +1,5 @@
+---
+"@web5/agent": patch
+---
+
+Add requestPermissionsForProtocol helper method to connect module
diff --git a/examples/wallet-connect.html b/examples/wallet-connect.html
index eb3e813ab..ccaa699e7 100644
--- a/examples/wallet-connect.html
+++ b/examples/wallet-connect.html
@@ -128,35 +128,12 @@
Success
},
};
- const scopes = [
- {
- interface: "Records",
- method: "Write",
- protocol: "http://profile-protocol.xyz",
- },
- {
- interface: "Records",
- method: "Query",
- protocol: "http://profile-protocol.xyz",
- },
- {
- interface: "Records",
- method: "Read",
- protocol: "http://profile-protocol.xyz",
- },
- ];
-
try {
const { delegateDid } = await Web5.connect({
walletConnectOptions: {
walletUri: "web5://connect",
connectServerUrl: "http://localhost:3000/connect",
- permissionRequests: [
- {
- protocolDefinition: profileProtocol,
- permissionScopes: scopes,
- },
- ],
+ permissionRequests: [{ protocolDefinition: profileProtocol }],
onWalletUriReady: generateQRCode,
validatePin: async () => {
goToPinScreen();
diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts
index c30d75908..fdaa35362 100644
--- a/packages/agent/src/connect.ts
+++ b/packages/agent/src/connect.ts
@@ -1,13 +1,16 @@
-import { CryptoUtils } from '@web5/crypto';
-import { DwnProtocolDefinition, DwnRecordsPermissionScope } from './index.js';
+
+import type { PushedAuthResponse } from './oidc.js';
+import type { DwnPermissionScope, DwnProtocolDefinition, Web5ConnectAuthResponse } from './index.js';
+
import {
- Web5ConnectAuthResponse,
Oidc,
- type PushedAuthResponse,
} from './oidc.js';
import { pollWithTtl } from './utils.js';
-import { DidJwk } from '@web5/dids';
+
import { Convert } from '@web5/common';
+import { CryptoUtils } from '@web5/crypto';
+import { DidJwk } from '@web5/dids';
+import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js';
/**
* Initiates the wallet connect process. Used when a client wants to obtain
@@ -179,7 +182,74 @@ export type ConnectPermissionRequest = {
protocolDefinition: DwnProtocolDefinition;
/** The scope of the permissions being requested for the given protocol */
- permissionScopes: DwnRecordsPermissionScope[];
+ permissionScopes: DwnPermissionScope[];
};
-export const WalletConnect = { initClient };
+export type Permission = 'write' | 'read' | 'delete' | 'query' | 'subscribe';
+
+function createPermissionRequestForProtocol(definition: DwnProtocolDefinition, permissions: Permission[]): ConnectPermissionRequest {
+ const requests: DwnPermissionScope[] = [];
+
+ // In order to enable sync, we must request permissions for `MessagesQuery`, `MessagesRead` and `MessagesSubscribe`
+ requests.push({
+ protocol : definition.protocol,
+ interface : DwnInterfaceName.Messages,
+ method : DwnMethodName.Read,
+ }, {
+ protocol : definition.protocol,
+ interface : DwnInterfaceName.Messages,
+ method : DwnMethodName.Query,
+ }, {
+ protocol : definition.protocol,
+ interface : DwnInterfaceName.Messages,
+ method : DwnMethodName.Subscribe,
+ });
+
+ // We also request any additional permissions the user has requested for this protocol
+ for (const permission of permissions) {
+ switch (permission) {
+ case 'write':
+ requests.push({
+ protocol : definition.protocol,
+ interface : DwnInterfaceName.Records,
+ method : DwnMethodName.Write,
+ });
+ break;
+ case 'read':
+ requests.push({
+ protocol : definition.protocol,
+ interface : DwnInterfaceName.Records,
+ method : DwnMethodName.Read,
+ });
+ break;
+ case 'delete':
+ requests.push({
+ protocol : definition.protocol,
+ interface : DwnInterfaceName.Records,
+ method : DwnMethodName.Delete,
+ });
+ break;
+ case 'query':
+ requests.push({
+ protocol : definition.protocol,
+ interface : DwnInterfaceName.Records,
+ method : DwnMethodName.Query,
+ });
+ break;
+ case 'subscribe':
+ requests.push({
+ protocol : definition.protocol,
+ interface : DwnInterfaceName.Records,
+ method : DwnMethodName.Subscribe,
+ });
+ break;
+ }
+ }
+
+ return {
+ protocolDefinition : definition,
+ permissionScopes : requests,
+ };
+}
+
+export const WalletConnect = { initClient, createPermissionRequestForProtocol };
diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts
index d544c428b..a05873393 100644
--- a/packages/agent/src/oidc.ts
+++ b/packages/agent/src/oidc.ts
@@ -12,15 +12,13 @@ import { concatenateUrl } from './utils.js';
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
import type { ConnectPermissionRequest } from './connect.js';
import { DidDocument, DidJwk, PortableDid, type BearerDid } from '@web5/dids';
-import { AgentDwnApi } from './dwn-api.js';
-import {
- DwnInterfaceName,
- DwnMethodName,
- type PermissionScope,
- type RecordsWriteMessage,
+import type {
+ PermissionScope,
+ RecordsWriteMessage,
} from '@tbd54566975/dwn-sdk-js';
import { DwnInterface, DwnProtocolDefinition } from './types/dwn.js';
import { AgentPermissionsApi } from './permissions-api.js';
+import type { Web5Agent } from './types/agent.js';
/**
* Sent to an OIDC server to authorize a client. Allows clients
@@ -616,14 +614,17 @@ function encryptAuthResponse({
async function createPermissionGrants(
selectedDid: string,
delegateBearerDid: BearerDid,
- dwn: AgentDwnApi,
- permissionsApi: AgentPermissionsApi,
+ agent: Web5Agent,
scopes: PermissionScope[],
- protocolUri: string
) {
+
+ const permissionsApi = new AgentPermissionsApi({ agent });
+
+ // TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/TBD54566975/web5-js/issues/849
const permissionGrants = await Promise.all(
scopes.map((scope) =>
permissionsApi.createGrant({
+ store : true,
grantedTo : delegateBearerDid.uri,
scope,
dateExpires : '2040-06-25T16:09:16.693356Z',
@@ -632,57 +633,23 @@ async function createPermissionGrants(
)
);
- // Grant Messages Query and Messages Read for sync to work
- permissionGrants.push(
- await permissionsApi.createGrant({
- grantedTo : delegateBearerDid.uri,
- scope : {
- interface : DwnInterfaceName.Messages,
- method : DwnMethodName.Query,
- protocol : protocolUri,
- },
- dateExpires : '2040-06-25T16:09:16.693356Z',
- author : selectedDid,
- })
- );
- permissionGrants.push(
- await permissionsApi.createGrant({
- grantedTo : delegateBearerDid.uri,
- scope : {
- interface : DwnInterfaceName.Messages,
- method : DwnMethodName.Read,
- protocol : protocolUri,
- },
- dateExpires : '2040-06-25T16:09:16.693356Z',
- author : selectedDid,
- })
- );
-
const messagePromises = permissionGrants.map(async (grant) => {
- // Quirk: we have to pull out encodedData out of the message the schema validator doesnt want it there
+ // Quirk: we have to pull out encodedData out of the message the schema validator doesn't want it there
const { encodedData, ...rawMessage } = grant.message;
const data = Convert.base64Url(encodedData).toUint8Array();
- const params = {
+ const { reply } = await agent.sendDwnRequest({
author : selectedDid,
target : selectedDid,
messageType : DwnInterface.RecordsWrite,
dataStream : new Blob([data]),
rawMessage,
- };
-
- const message = await dwn.processRequest(params);
- const sent = await dwn.sendRequest(params);
+ });
- // TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/TBD54566975/web5-js/issues/849
- if (message.reply.status.code !== 202) {
- throw new Error(
- `Could not process the message. Error details: ${message.reply.status.detail}`
- );
- }
- if (sent.reply.status.code !== 202) {
+ // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync
+ if (reply.status.code !== 202 && reply.status.code !== 409) {
throw new Error(
- `Could not send the message. Error details: ${message.reply.status.detail}`
+ `Could not send the message. Error details: ${reply.status.detail}`
);
}
@@ -700,10 +667,10 @@ async function createPermissionGrants(
*/
async function prepareProtocols(
selectedDid: string,
- agentDwnApi: AgentDwnApi,
+ agent: Web5Agent,
protocolDefinition: DwnProtocolDefinition
) {
- const queryMessage = await agentDwnApi.processRequest({
+ const queryMessage = await agent.processDwnRequest({
author : selectedDid,
messageType : DwnInterface.ProtocolsQuery,
target : selectedDid,
@@ -711,7 +678,7 @@ async function prepareProtocols(
});
if (queryMessage.reply.status.code === 404) {
- const configureMessage = await agentDwnApi.processRequest({
+ const configureMessage = await agent.processDwnRequest({
author : selectedDid,
messageType : DwnInterface.ProtocolsConfigure,
target : selectedDid,
@@ -721,6 +688,19 @@ async function prepareProtocols(
if (configureMessage.reply.status.code !== 202) {
throw new Error(`Could not install protocol: ${configureMessage.reply.status.detail}`);
}
+
+ // send the configure message to the remote DWN so that the APP can immediately use it without waiting for a sync cycle from the wallet
+ const { reply: sendReply } = await agent.sendDwnRequest({
+ author : selectedDid,
+ target : selectedDid,
+ messageType : DwnInterface.ProtocolsConfigure,
+ rawMessage : configureMessage.message,
+ });
+
+ // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync
+ if (sendReply.status.code !== 202 && sendReply.status.code !== 409) {
+ throw new Error(`Could not send protocol: ${sendReply.status.detail}`);
+ }
} else if (queryMessage.reply.status.code !== 200) {
throw new Error(`Could not fetch protcol: ${queryMessage.reply.status.detail}`);
}
@@ -739,24 +719,17 @@ async function submitAuthResponse(
selectedDid: string,
authRequest: Web5ConnectAuthRequest,
randomPin: string,
- agentDwnApi: AgentDwnApi,
- agentPermissionsApi: AgentPermissionsApi
+ agent: Web5Agent,
) {
const delegateBearerDid = await DidJwk.create();
const delegatePortableDid = await delegateBearerDid.export();
const delegateGrantPromises = authRequest.permissionRequests.map(async (permissionRequest) => {
- await prepareProtocols(selectedDid, agentDwnApi, permissionRequest.protocolDefinition);
-
// TODO: validate to make sure the scopes and definition are assigned to the same protocol
- const permissionGrants = await Oidc.createPermissionGrants(
- selectedDid,
- delegateBearerDid,
- agentDwnApi,
- agentPermissionsApi,
- permissionRequest.permissionScopes,
- permissionRequest.protocolDefinition.protocol
- );
+ const { protocolDefinition, permissionScopes } = permissionRequest;
+
+ await prepareProtocols(selectedDid, agent, protocolDefinition);
+ const permissionGrants = await Oidc.createPermissionGrants(selectedDid, delegateBearerDid, agent, permissionScopes);
return permissionGrants;
});
diff --git a/packages/agent/src/sync-api.ts b/packages/agent/src/sync-api.ts
index 3c7ea1089..29763f6b8 100644
--- a/packages/agent/src/sync-api.ts
+++ b/packages/agent/src/sync-api.ts
@@ -41,7 +41,7 @@ export class AgentSyncApi implements SyncEngine {
this._syncEngine.agent = agent;
}
- public async registerIdentity(params: { did: string; options: SyncIdentityOptions }): Promise {
+ public async registerIdentity(params: { did: string; options?: SyncIdentityOptions }): Promise {
await this._syncEngine.registerIdentity(params);
}
diff --git a/packages/agent/src/sync-engine-level.ts b/packages/agent/src/sync-engine-level.ts
index 259667cb8..5908fba49 100644
--- a/packages/agent/src/sync-engine-level.ts
+++ b/packages/agent/src/sync-engine-level.ts
@@ -250,10 +250,13 @@ export class SyncEngineLevel implements SyncEngine {
await pushQueue.batch(deleteOperations as any);
}
- public async registerIdentity({ did, options }: { did: string; options: SyncIdentityOptions }): Promise {
+ public async registerIdentity({ did, options }: { did: string; options?: SyncIdentityOptions }): Promise {
// Get a reference to the `registeredIdentities` sublevel.
const registeredIdentities = this._db.sublevel('registeredIdentities');
+ // if no options are provided, we default to no delegateDid and all protocols (empty array)
+ options ??= { protocols: [] };
+
// Add (or overwrite, if present) the Identity's DID as a registered identity.
await registeredIdentities.put(did, JSON.stringify(options));
}
diff --git a/packages/agent/src/types/sync.ts b/packages/agent/src/types/sync.ts
index ee4dd3182..1fc5666d3 100644
--- a/packages/agent/src/types/sync.ts
+++ b/packages/agent/src/types/sync.ts
@@ -6,7 +6,7 @@ export type SyncIdentityOptions = {
}
export interface SyncEngine {
agent: Web5PlatformAgent;
- registerIdentity(params: { did: string, options: SyncIdentityOptions }): Promise;
+ registerIdentity(params: { did: string, options?: SyncIdentityOptions }): Promise;
sync(direction?: 'push' | 'pull'): Promise;
startSync(params: { interval: string }): Promise;
stopSync(): void;
diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts
index 3d6c0d20a..441f80bdd 100644
--- a/packages/agent/tests/connect.spec.ts
+++ b/packages/agent/tests/connect.spec.ts
@@ -4,7 +4,6 @@ import { CryptoUtils } from '@web5/crypto';
import { type BearerDid, DidDht, DidJwk, PortableDid } from '@web5/dids';
import { Convert } from '@web5/common';
import {
- DelegateGrant,
Oidc,
type Web5ConnectAuthRequest,
type Web5ConnectAuthResponse,
@@ -12,8 +11,9 @@ import {
import { PlatformAgentTestHarness } from '../src/test-harness.js';
import { TestAgent } from './utils/test-agent.js';
import { testDwnUrl } from './utils/test-config.js';
-import { BearerIdentity, DwnProtocolDefinition, DwnProtocolPermissionScope, DwnResponse, WalletConnect } from '../src/index.js';
-import { RecordsPermissionScope, type PermissionScope } from '@tbd54566975/dwn-sdk-js';
+import { BearerIdentity, DwnProtocolDefinition, WalletConnect } from '../src/index.js';
+import { RecordsPermissionScope } from '@tbd54566975/dwn-sdk-js';
+import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js';
describe('web5 connect', function () {
this.timeout(20000);
@@ -197,6 +197,7 @@ describe('web5 connect', function () {
});
after(async () => {
+ sinon.restore();
await testHarness.clearStorage();
await testHarness.closeStorage();
});
@@ -288,13 +289,11 @@ describe('web5 connect', function () {
const results = await Oidc.createPermissionGrants(
providerIdentity.did.uri,
delegateBearerDid,
- testHarness.agent.dwn,
- testHarness.agent.permissions,
- permissionScopes,
- protocolDefinition.protocol
+ testHarness.agent,
+ permissionScopes
);
- const scopesRequestedPlusTwoDefaultScopes = permissionScopes.length + 2;
- expect(results).to.have.lengthOf(scopesRequestedPlusTwoDefaultScopes);
+ const scopesRequested = permissionScopes.length;
+ expect(results).to.have.lengthOf(scopesRequested);
expect(results[0]).to.be.a('object');
});
@@ -392,8 +391,7 @@ describe('web5 connect', function () {
selectedDid,
authRequest,
randomPin,
- testHarness.agent.dwn,
- testHarness.agent.permissions
+ testHarness.agent
);
expect(fetchSpy.calledOnce).to.be.true;
});
@@ -490,4 +488,83 @@ describe('web5 connect', function () {
expect(results?.delegatePortableDid).to.be.an('object');
});
});
+
+ describe('createPermissionRequestForProtocol', () => {
+ it('should add sync permissions to all requests', async () => {
+ const protocol:DwnProtocolDefinition = {
+ published : true,
+ protocol : 'https://exmaple.org/protocols/social',
+ types : {
+ note: {
+ schema : 'https://example.org/schemas/note',
+ dataFormats : [ 'application/json', 'text/plain' ],
+ }
+ },
+ structure: {
+ note: {}
+ }
+ };
+
+ const permissionRequests = WalletConnect.createPermissionRequestForProtocol(protocol, []);
+
+ expect(permissionRequests.protocolDefinition).to.deep.equal(protocol);
+ expect(permissionRequests.permissionScopes.length).to.equal(3); // only includes the sync permissions
+ expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Read)).to.not.be.undefined;
+ expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Query)).to.not.be.undefined;
+ expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Messages && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined;
+ });
+
+ it('should add requested permissions to the request', async () => {
+ const protocol:DwnProtocolDefinition = {
+ published : true,
+ protocol : 'https://exmaple.org/protocols/social',
+ types : {
+ note: {
+ schema : 'https://example.org/schemas/note',
+ dataFormats : [ 'application/json', 'text/plain' ],
+ }
+ },
+ structure: {
+ note: {}
+ }
+ };
+
+ const permissionRequests = WalletConnect.createPermissionRequestForProtocol(protocol, ['write', 'read']);
+
+ expect(permissionRequests.protocolDefinition).to.deep.equal(protocol);
+
+ // the 3 sync permissions plus the 2 requested permissions
+ expect(permissionRequests.permissionScopes.length).to.equal(5);
+ expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined;
+ expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined;
+ });
+
+ it('supports requesting `read`, `write`, `delete`, `query` and `subscribe` permissions', async () => {
+ const protocol:DwnProtocolDefinition = {
+ published : true,
+ protocol : 'https://exmaple.org/protocols/social',
+ types : {
+ note: {
+ schema : 'https://example.org/schemas/note',
+ dataFormats : [ 'application/json', 'text/plain' ],
+ }
+ },
+ structure: {
+ note: {}
+ }
+ };
+
+ const permissionRequests = WalletConnect.createPermissionRequestForProtocol(protocol, ['write', 'read', 'delete', 'query', 'subscribe']);
+
+ expect(permissionRequests.protocolDefinition).to.deep.equal(protocol);
+
+ // the 3 sync permissions plus the 5 requested permissions
+ expect(permissionRequests.permissionScopes.length).to.equal(8);
+ expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Read)).to.not.be.undefined;
+ expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Write)).to.not.be.undefined;
+ expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Delete)).to.not.be.undefined;
+ expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Query)).to.not.be.undefined;
+ expect(permissionRequests.permissionScopes.find(scope => scope.interface === DwnInterfaceName.Records && scope.method === DwnMethodName.Subscribe)).to.not.be.undefined;
+ });
+ });
});
diff --git a/packages/agent/tests/rpc-client.spec.ts b/packages/agent/tests/rpc-client.spec.ts
index 1d03fb534..8bb207c38 100644
--- a/packages/agent/tests/rpc-client.spec.ts
+++ b/packages/agent/tests/rpc-client.spec.ts
@@ -69,6 +69,10 @@ describe('RPC Clients', () => {
alice = await TestDataGenerator.generateDidKeyPersona();
});
+ after(() => {
+ sinon.restore();
+ });
+
it('returns available transports', async () => {
const httpOnlyClient = new Web5RpcClient();
@@ -262,6 +266,10 @@ describe('RPC Clients', () => {
let alice: Persona;
let client: HttpWeb5RpcClient;
+ after(() => {
+ sinon.restore();
+ });
+
beforeEach(async () => {
sinon.restore();
@@ -353,6 +361,10 @@ describe('RPC Clients', () => {
dwnUrl.protocol = dwnUrl.protocol === 'http:' ? 'ws:' : 'wss:';
const socketDwnUrl = dwnUrl.toString();
+ after(() => {
+ sinon.restore();
+ });
+
beforeEach(async () => {
sinon.restore();
diff --git a/packages/agent/tests/sync-engine-level.spec.ts b/packages/agent/tests/sync-engine-level.spec.ts
index 7365148a6..35fa68a08 100644
--- a/packages/agent/tests/sync-engine-level.spec.ts
+++ b/packages/agent/tests/sync-engine-level.spec.ts
@@ -12,6 +12,7 @@ import { testDwnUrl } from './utils/test-config.js';
import { SyncEngineLevel } from '../src/sync-engine-level.js';
import { PlatformAgentTestHarness } from '../src/test-harness.js';
import { Convert } from '@web5/common';
+import { AbstractLevel } from 'abstract-level';
let testDwnUrls: string[] = [testDwnUrl];
@@ -162,6 +163,7 @@ describe('SyncEngineLevel', () => {
});
after(async () => {
+ sinon.restore();
await testHarness.clearStorage();
await testHarness.closeStorage();
});
@@ -353,10 +355,7 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// Execute Sync to pull all records from Alice's remote DWN to Alice's local DWN.
@@ -431,10 +430,7 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// Execute Sync to push and pull all records from Alice's remote DWN to Alice's local DWN.
@@ -471,10 +467,7 @@ describe('SyncEngineLevel', () => {
it('throws if sync is attempted while an interval sync is running', async () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// start the sync engine with an interval of 10 seconds
@@ -1057,6 +1050,21 @@ describe('SyncEngineLevel', () => {
expect(localBarRecords.reply.status.code).to.equal(200);
expect(localBarRecords.reply.entries).to.have.length(0);
});
+
+ it('defaults to all protocols and undefined delegate if no options are provided', async () => {
+ // spy on AbstractLevel put
+ const abstractLevelPut = sinon.spy(AbstractLevel.prototype, 'put');
+
+ // register identity without any options
+ await testHarness.agent.sync.registerIdentity({
+ did: alice.did.uri
+ });
+
+ const registerIdentitiesPutCall = abstractLevelPut.args[0];
+ const options = JSON.parse(registerIdentitiesPutCall[1] as string);
+ // confirm that without options the options are set to an empty protocol array
+ expect(options).to.deep.equal({ protocols: [] });
+ });
});
describe('pull()', () => {
@@ -1112,10 +1120,7 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// Execute Sync to pull all records from Alice's remote DWN to Alice's local DWN.
@@ -1208,10 +1213,7 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// Execute Sync to pull all records from Alice's remote DWNs
@@ -1337,10 +1339,7 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// spy on sendDwnRequest to the remote DWN
@@ -1561,10 +1560,7 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// Execute Sync to pull all records from Alice's remote DWN to Alice's local DWN.
@@ -1626,10 +1622,7 @@ describe('SyncEngineLevel', () => {
// register alice
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// create a remote record
@@ -1720,18 +1713,12 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// Register Bob's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : bob.did.uri,
- options : {
- protocols: []
- }
+ did: bob.did.uri,
});
// Execute Sync to pull all records from Alice's and Bob's remove DWNs to their local DWNs.
@@ -1814,10 +1801,7 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// Execute Sync to pull all records from Alice's remote DWN to Alice's local DWN.
@@ -1917,10 +1901,7 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// Execute Sync to pull all records from Alice's remote DWNs
@@ -1956,10 +1937,7 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// scenario: The messageCids returned from the local eventLog contains a Cid that already exists in the remote DWN.
@@ -2251,10 +2229,7 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// Execute Sync to push all records from Alice's local DWN to Alice's remote DWN.
@@ -2315,10 +2290,7 @@ describe('SyncEngineLevel', () => {
//register alice
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// create a local record
@@ -2407,18 +2379,12 @@ describe('SyncEngineLevel', () => {
// Register Alice's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
// Register Bob's DID to be synchronized.
await testHarness.agent.sync.registerIdentity({
- did : bob.did.uri,
- options : {
- protocols: []
- }
+ did: bob.did.uri,
});
// Execute Sync to push all records from Alice's and Bob's local DWNs to their remote DWNs.
@@ -2451,10 +2417,7 @@ describe('SyncEngineLevel', () => {
describe('startSync()', () => {
it('calls sync() in each interval', async () => {
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
const syncSpy = sinon.stub(SyncEngineLevel.prototype, 'sync');
@@ -2473,10 +2436,7 @@ describe('SyncEngineLevel', () => {
it('does not call sync() again until a sync round finishes', async () => {
await testHarness.agent.sync.registerIdentity({
- did : alice.did.uri,
- options : {
- protocols: []
- }
+ did: alice.did.uri,
});
const clock = sinon.useFakeTimers();
diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts
index 112579de1..6aba6ecef 100644
--- a/packages/api/src/web5.ts
+++ b/packages/api/src/web5.ts
@@ -9,8 +9,10 @@ import type {
DelegateGrant,
DwnDataEncodedRecordsWriteMessage,
DwnMessagesPermissionScope,
+ DwnProtocolDefinition,
DwnRecordsPermissionScope,
HdIdentityVault,
+ Permission,
WalletConnectOptions,
Web5Agent,
} from '@web5/agent';
@@ -35,6 +37,15 @@ export type DidCreateOptions = {
dwnEndpoints?: string[];
}
+export type ConnectPermissionRequest = {
+ protocolDefinition: DwnProtocolDefinition;
+ permissions?: Permission[];
+}
+
+export type ConnectOptions = Omit & {
+ permissionRequests: ConnectPermissionRequest[];
+}
+
/** Optional overrides that can be provided when calling {@link Web5.connect}. */
export type Web5ConnectOptions = {
/**
@@ -42,7 +53,7 @@ export type Web5ConnectOptions = {
* This param currently will not work in apps that are currently connected.
* It must only be invoked at registration with a reset and empty DWN and agent.
*/
- walletConnectOptions?: WalletConnectOptions;
+ walletConnectOptions?: ConnectOptions;
/**
* Provide a {@link Web5Agent} implementation. Defaults to creating a local
@@ -276,7 +287,15 @@ export class Web5 {
// No connected identity found and connectOptions are provided, attempt to import a delegated DID from an external wallet
try {
- const { delegatePortableDid, connectedDid, delegateGrants: returnedGrants } = await WalletConnect.initClient(walletConnectOptions);
+ const { permissionRequests, ...connectOptions } = walletConnectOptions;
+ const walletPermissionRequests = permissionRequests.map(({ protocolDefinition, permissions }) => WalletConnect.createPermissionRequestForProtocol(protocolDefinition, permissions ?? [
+ 'read', 'write', 'delete', 'query', 'subscribe'
+ ]));
+
+ const { delegatePortableDid, connectedDid, delegateGrants: returnedGrants } = await WalletConnect.initClient({
+ ...connectOptions,
+ permissionRequests: walletPermissionRequests,
+ });
delegateGrants = returnedGrants;
// Import the delegated DID as an Identity in the User Agent.
diff --git a/packages/api/tests/web5.spec.ts b/packages/api/tests/web5.spec.ts
index ef1bdb522..ceb4791e9 100644
--- a/packages/api/tests/web5.spec.ts
+++ b/packages/api/tests/web5.spec.ts
@@ -793,6 +793,145 @@ describe('web5 api', () => {
expect(startSyncSpy.args[0][0].interval).to.equal('1m');
});
+
+
+
+ it('should request all permissions for a protocol if no specific permissions are provided', async () => {
+
+ sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent);
+
+ // spy on the WalletConnect createPermissionRequestForProtocol method
+ const requestPermissionsSpy = sinon.spy(WalletConnect, 'createPermissionRequestForProtocol');
+
+ // We throw and spy on the initClient method to avoid the actual WalletConnect initialization
+ // but to still be able to spy on the passed parameters
+ sinon.stub(WalletConnect, 'initClient').throws('Error');
+
+ // stub the cleanUpIdentity method to avoid actual cleanup
+ sinon.stub(Web5 as any, 'cleanUpIdentity').resolves();
+
+ const protocolDefinition: DwnProtocolDefinition = {
+ protocol : 'https://example.com/test-protocol',
+ published : true,
+ types : {
+ foo : {},
+ bar : {}
+ },
+ structure: {
+ foo: {
+ bar: {}
+ }
+ }
+ };
+
+ try {
+
+ await Web5.connect({
+ walletConnectOptions: {
+ connectServerUrl : 'https://connect.example.com',
+ walletUri : 'https://wallet.example.com',
+ validatePin : async () => { return '1234'; },
+ onWalletUriReady : (_walletUri: string) => {},
+ permissionRequests : [{ protocolDefinition }]
+ }
+ });
+
+ expect.fail('Should have thrown an error');
+ } catch(error: any) {
+ // we expect an error because we stubbed the initClient method to throw it
+ expect(error.message).to.include('Sinon-provided Error');
+
+ // The `createPermissionRequestForProtocol` method should have been called once for the provided protocol
+ expect(requestPermissionsSpy.callCount).to.equal(1);
+ const call = requestPermissionsSpy.getCall(0);
+
+ // since no explicit permissions were provided, all permissions should be requested
+ expect(call.args[1]).to.have.members([
+ 'read', 'write', 'delete', 'query', 'subscribe'
+ ]);
+ }
+ });
+
+ it('should only request the specified permissions for a protocol', async () => {
+
+ sinon.stub(Web5UserAgent, 'create').resolves(testHarness.agent as Web5UserAgent);
+
+ // spy on the WalletConnect createPermissionRequestForProtocol method
+ const requestPermissionsSpy = sinon.spy(WalletConnect, 'createPermissionRequestForProtocol');
+
+ // We throw and spy on the initClient method to avoid the actual WalletConnect initialization
+ // but to still be able to spy on the passed parameters
+ sinon.stub(WalletConnect, 'initClient').throws('Error');
+
+ // stub the cleanUpIdentity method to avoid actual cleanup
+ sinon.stub(Web5 as any, 'cleanUpIdentity').resolves();
+
+ const protocol1Definition: DwnProtocolDefinition = {
+ protocol : 'https://example.com/test-protocol-1',
+ published : true,
+ types : {
+ foo : {},
+ bar : {}
+ },
+ structure: {
+ foo: {
+ bar: {}
+ }
+ }
+ };
+
+ const protocol2Definition: DwnProtocolDefinition = {
+ protocol : 'https://example.com/test-protocol-2',
+ published : true,
+ types : {
+ foo : {},
+ bar : {}
+ },
+ structure: {
+ foo: {
+ bar: {}
+ }
+ }
+ };
+
+
+ try {
+
+ await Web5.connect({
+ walletConnectOptions: {
+ connectServerUrl : 'https://connect.example.com',
+ walletUri : 'https://wallet.example.com',
+ validatePin : async () => { return '1234'; },
+ onWalletUriReady : (_walletUri: string) => {},
+ permissionRequests : [
+ { protocolDefinition: protocol1Definition }, // no permissions provided, expect all permissions to be requested
+ { protocolDefinition: protocol2Definition, permissions: ['read', 'write'] } // only read and write permissions provided
+ ]
+ }
+ });
+
+ expect.fail('Should have thrown an error');
+ } catch(error: any) {
+ // we expect an error because we stubbed the initClient method to throw it
+ expect(error.message).to.include('Sinon-provided Error');
+
+ // The `createPermissionRequestForProtocol` method should have been called once for each provided request
+ expect(requestPermissionsSpy.callCount).to.equal(2);
+ const call1 = requestPermissionsSpy.getCall(0);
+
+ // since no explicit permissions were provided for the first protocol, all permissions should be requested
+ expect(call1.args[1]).to.have.members([
+ 'read', 'write', 'delete', 'query', 'subscribe'
+ ]);
+
+ const call2 = requestPermissionsSpy.getCall(1);
+
+ // only the provided permissions should be requested for the second protocol
+ expect(call2.args[1]).to.have.members([
+ 'read', 'write'
+ ]);
+ }
+ });
});
describe('registration', () => {