Skip to content

Commit

Permalink
Add requestPermissionsForProtocol helper method to connect module (#854)
Browse files Browse the repository at this point in the history
This PR adds a `requestPermissionsForProtocol` helper method to the Connect module.

This helper method takes a protocol definition, as well as simple string representations of the permissions being requested, ie `write`, `read`, `delete`, `query` and `subscribe`.

It will by default include the permissions also needed to sync a protocol's messages `MessagesRead`, `MessagesQuery` and `MessagesSubscribe`.

#### Example:
```typescript
// all permissions for each protocol    
const { delegateDid } = await Web5.connect({
  walletConnectOptions: {
    walletUri: "web5://connect",
    connectServerUrl: "http://localhost:3000/connect",
    permissionRequests: [{ protocolDefinition: profileProtocol }],
    onWalletUriReady: generateQRCode,
    validatePin: async () => {
      goToPinScreen();

      const pin = await waitForPin();
      return pin;
    },
  },
});
```
```typescript
// specific permissions
const { delegateDid } = await Web5.connect({
  walletConnectOptions: {
    walletUri: "web5://connect",
    connectServerUrl: "http://localhost:3000/connect",
    permissionRequests: [{
      definition: protocol1,
      permissions: ['read', 'write'] // creates read+write + sync grants
    },{
      definition: protocol2,
    }],
    onWalletUriReady: generateQRCode,
    validatePin: async () => {
      goToPinScreen();

      const pin = await waitForPin();
      return pin;
    },
  },
});
```
---

This PR also makes `registerIdentity` options optional. If no options are provided all protocols are synced.
  • Loading branch information
LiranCohen authored Aug 27, 2024
1 parent fea0535 commit 4ff2316
Show file tree
Hide file tree
Showing 12 changed files with 422 additions and 187 deletions.
5 changes: 5 additions & 0 deletions .changeset/fresh-olives-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@web5/agent": patch
---

Add requestPermissionsForProtocol helper method to connect module
25 changes: 1 addition & 24 deletions examples/wallet-connect.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,35 +128,12 @@ <h1>Success</h1>
},
};

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();
Expand Down
84 changes: 77 additions & 7 deletions packages/agent/src/connect.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 };
101 changes: 37 additions & 64 deletions packages/agent/src/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand All @@ -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}`
);
}

Expand All @@ -700,18 +667,18 @@ 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,
messageParams : { filter: { protocol: protocolDefinition.protocol } },
});

if (queryMessage.reply.status.code === 404) {
const configureMessage = await agentDwnApi.processRequest({
const configureMessage = await agent.processDwnRequest({
author : selectedDid,
messageType : DwnInterface.ProtocolsConfigure,
target : selectedDid,
Expand All @@ -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}`);
}
Expand All @@ -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;
});
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/sync-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class AgentSyncApi implements SyncEngine {
this._syncEngine.agent = agent;
}

public async registerIdentity(params: { did: string; options: SyncIdentityOptions }): Promise<void> {
public async registerIdentity(params: { did: string; options?: SyncIdentityOptions }): Promise<void> {
await this._syncEngine.registerIdentity(params);
}

Expand Down
5 changes: 4 additions & 1 deletion packages/agent/src/sync-engine-level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
public async registerIdentity({ did, options }: { did: string; options?: SyncIdentityOptions }): Promise<void> {
// 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));
}
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/types/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export type SyncIdentityOptions = {
}
export interface SyncEngine {
agent: Web5PlatformAgent;
registerIdentity(params: { did: string, options: SyncIdentityOptions }): Promise<void>;
registerIdentity(params: { did: string, options?: SyncIdentityOptions }): Promise<void>;
sync(direction?: 'push' | 'pull'): Promise<void>;
startSync(params: { interval: string }): Promise<void>;
stopSync(): void;
Expand Down
Loading

0 comments on commit 4ff2316

Please sign in to comment.