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

Add requestPermissionsForProtocol helper method to connect module #854

Merged
merged 9 commits into from
Aug 27, 2024
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 { 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 @@
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 @@
)
);

// 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}`

Check warning on line 652 in packages/agent/src/oidc.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/oidc.ts#L652

Added line #L652 was not covered by tests
);
}

Expand All @@ -700,18 +667,18 @@
*/
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({

Check warning on line 681 in packages/agent/src/oidc.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/oidc.ts#L681

Added line #L681 was not covered by tests
author : selectedDid,
messageType : DwnInterface.ProtocolsConfigure,
target : selectedDid,
Expand All @@ -721,6 +688,19 @@
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}`);
}

Check warning on line 703 in packages/agent/src/oidc.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/oidc.ts#L691-L703

Added lines #L691 - L703 were not covered by tests
} else if (queryMessage.reply.status.code !== 200) {
throw new Error(`Could not fetch protcol: ${queryMessage.reply.status.detail}`);
}
Expand All @@ -739,24 +719,17 @@
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
Loading