From 217aa2e9b2e68a2312c8dcc0e1c5aff32245a04e Mon Sep 17 00:00:00 2001 From: Tim Shamilov Date: Wed, 21 Aug 2024 23:35:37 +0800 Subject: [PATCH] init connect dwn integration --- examples/wallet-connect.html | 13 +++- package.json | 2 +- packages/agent/src/connect.ts | 15 +++- packages/agent/src/oidc.ts | 135 ++++++++++++++++++++++++---------- packages/api/src/web5.ts | 8 +- 5 files changed, 127 insertions(+), 46 deletions(-) diff --git a/examples/wallet-connect.html b/examples/wallet-connect.html index 32d530f22..3abd3335e 100644 --- a/examples/wallet-connect.html +++ b/examples/wallet-connect.html @@ -139,10 +139,15 @@

Success

method: "Query", protocol: "http://profile-protocol.xyz", }, + { + interface: "Records", + method: "Read", + protocol: "http://profile-protocol.xyz", + }, ]; try { - const { delegateDid } = await Web5.connect({ + const { delegateDid, delegateGrants } = await Web5.connect({ walletConnectOptions: { walletUri: "web5://connect", connectServerUrl: "http://localhost:3000/connect", @@ -162,7 +167,7 @@

Success

}, }); - goToEndScreen(delegateDid); + goToEndScreen(delegateDid, delegateGrants); } catch (e) { document.getElementById( "errorMessage" @@ -205,10 +210,10 @@

Success

document.getElementById("pinScreen").style.display = "block"; } - function goToEndScreen(delegateDid) { + function goToEndScreen(delegateDid, delegateGrants) { document.getElementById("didInformation").innerText = `${JSON.stringify( delegateDid - )}`; + )} \n \n \n \n ${JSON.stringify(delegateGrants)}`; document.getElementById("pinScreen").style.display = "none"; document.getElementById("endScreen").style.display = "block"; diff --git a/package.json b/package.json index 92a4b3bff..9b1ef69bd 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "build": "pnpm --recursive --stream build", "test:node": "pnpm --recursive test:node", "audit-ci": "audit-ci --config ./audit-ci.json", - "wallet:connect:example": "npx http-server & HTTP_SERVER_PID=$! && sleep 2 && open 'http://localhost:8080/examples/wallet-connect.html' && wait $HTTP_SERVER_PID" + "wallet:connect:example": "npx http-server -c-1 & HTTP_SERVER_PID=$! && sleep 2 && open 'http://localhost:8080/examples/wallet-connect.html' && wait $HTTP_SERVER_PID" }, "repository": { "type": "git", diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index e3f8e51cc..ee8bd0e7e 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -90,7 +90,10 @@ async function initClient({ // a route to its web5 connect provider flow and the params of where to fetch the auth request. const generatedWalletUri = new URL(walletUri); generatedWalletUri.searchParams.set('request_uri', parData.request_uri); - generatedWalletUri.searchParams.set('encryption_key', Convert.uint8Array(encryptionKey).toBase64Url()); + generatedWalletUri.searchParams.set( + 'encryption_key', + Convert.uint8Array(encryptionKey).toBase64Url() + ); // call user's callback so they can send the URI to the wallet as they see fit onWalletUriReady(generatedWalletUri.toString()); @@ -179,4 +182,14 @@ export type ConnectPermissionRequest = { permissionScopes: DwnRecordsPermissionScope[]; }; +// TODO: add a convenience method for generating scopes and protcol definitions? + +// const SomeObject = 'i\'m a protocol'; + +// Web5.connect({ +// walletConnectOptions: { +// permissionRequests: [{ protcolDefinition: SomeObject, permissionScopes: [new PermissionScope()] }], +// }, +// }); + export const WalletConnect = { initClient }; diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index 96b948941..1a793411b 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -13,8 +13,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 } from '@tbd54566975/dwn-sdk-js'; +import { + DwnInterfaceName, + DwnMethodName, + PermissionScope, +} from '@tbd54566975/dwn-sdk-js'; import { DwnInterface } from './types/dwn.js'; +import { AgentPermissionsApi } from './permissions-api.js'; /** * Sent to an OIDC server to authorize a client. Allows clients @@ -240,10 +245,7 @@ async function generateCodeChallenge() { async function createAuthRequest( options: RequireOnly< Web5ConnectAuthRequest, - | 'client_id' - | 'scope' - | 'redirect_uri' - | 'permissionRequests' + 'client_id' | 'scope' | 'redirect_uri' | 'permissionRequests' > ) { // Generate a random state value to associate the authorization request with the response. @@ -606,39 +608,83 @@ function encryptAuthResponse({ * Creates the permission grants that assign to the selectedDid the level of * permissions that the web app requested in the {@link Web5ConnectAuthRequest} */ -export async function createPermissionGrants( +// TODO: need another helper to call multiple times for each protocol +async function createPermissionGrants( selectedDid: string, delegateDid: BearerDid, - dwn: AgentDwnApi + dwn: AgentDwnApi, + permissionsApi: AgentPermissionsApi, + // we assume something generated the scopes + scopes: PermissionScope[], + protocolUri: string ) { - // TODO: remove mock after adding functionality: https://github.com/TBD54566975/web5-js/issues/827 - const permissionRequestData = { - description: - 'The app is asking to Records Write to http://profile-protocol.xyz', - scope: { - interface : DwnInterfaceName.Records, - method : DwnMethodName.Write, - protocol : 'http://profile-protocol.xyz', - }, - }; + // PermissionsApi.createRequest(); + // grantedTo: delegateDid + + // 1. fllw calls connect + // 2. fllw knows what it needs to operate: ['read', 'write', 'query'] + const permissionGrants = await Promise.all( + scopes.map((scope) => + permissionsApi.createGrant({ + grantedTo : delegateDid.uri, + scope, + dateExpires : '2040-06-25T16:09:16.693356Z', + author : selectedDid, + }) + ) + ); - // TODO: remove mock after adding functionality: https://github.com/TBD54566975/web5-js/issues/827 - const message = await dwn.processRequest({ - author : selectedDid, - target : selectedDid, - messageType : DwnInterface.RecordsWrite, - messageParams : { - recipient : delegateDid.uri, - protocolPath : 'grant', - protocol : ' https://tbd.website/dwn/permissions', - dataFormat : 'application/json', - data : Convert.object(permissionRequestData).toUint8Array(), + // By default we must grant Messages Query and Messages Read for sync + permissionGrants.push(await permissionsApi.createGrant({ + grantedTo : delegateDid.uri, + scope : { + interface : DwnInterfaceName.Messages, + method : DwnMethodName.Query, + protocol : protocolUri }, - // todo: is it data or datastream? - // dataStream: await Convert.object(permissionRequestData).toBlobAsync(), - }); + dateExpires : '2040-06-25T16:09:16.693356Z', + author : selectedDid, + })); + permissionGrants.push(await permissionsApi.createGrant({ + grantedTo : delegateDid.uri, + scope : { + interface : DwnInterfaceName.Messages, + method : DwnMethodName.Read, + protocol : protocolUri + }, + dateExpires : '2040-06-25T16:09:16.693356Z', + author : selectedDid, + })); + + for (const grant of permissionGrants) { + // Quirk: we have to pull out encodedData out of the message the schema validator doesnt want it there + const { encodedData, ...rawMessage } = grant.message; + + const data = Convert.base64Url(encodedData).toUint8Array(); + const params = { + author : selectedDid, + target : selectedDid, + messageType : DwnInterface.RecordsWrite, + dataStream : new Blob([data]), + rawMessage + }; + + // TODO: remove mock after adding functionality: https://github.com/TBD54566975/web5-js/issues/827 + 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) { + throw new Error(`Could not send the message. Error details: ${message.reply.status.detail}`); + } + } - return [message]; + const messages = permissionGrants.map((grant) => grant.message); + + return messages; } /** @@ -654,16 +700,27 @@ async function submitAuthResponse( selectedDid: string, authRequest: Web5ConnectAuthRequest, randomPin: string, - dwn: AgentDwnApi + agentDwnApi: AgentDwnApi, + agentPermissionsApi: AgentPermissionsApi ) { const delegateDid = await DidJwk.create(); const delegateDidPortable = await delegateDid.export(); - const permissionGrants = await Oidc.createPermissionGrants( - selectedDid, - delegateDid, - dwn - ); + let grantArr = []; + + for (const permissionRequest of authRequest.permissionRequests) { + // TODO: validate to make sure the scopes and definition are assigned to the same protocol + const permissionGrants = await Oidc.createPermissionGrants( + selectedDid, + delegateDid, + agentDwnApi, + agentPermissionsApi, + permissionRequest.permissionScopes, + permissionRequest.protocolDefinition.protocol + ); + + grantArr.push(...permissionGrants); + } const responseObject = await Oidc.createResponseObject({ //* the IDP's did that was selected to be connected @@ -674,7 +731,7 @@ async function submitAuthResponse( aud : authRequest.client_id, //* the nonce of the original auth request nonce : authRequest.nonce, - delegateGrants : permissionGrants, + delegateGrants : grantArr, delegateDid : delegateDidPortable, }); diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index e905332c5..558ed6da6 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -154,6 +154,8 @@ export type Web5ConnectResult = { * {@link WalletConnectOptions} was provided. */ delegateDid?: string; + + delegateGrants: any; }; /** @@ -226,6 +228,7 @@ export class Web5 { walletConnectOptions, }: Web5ConnectOptions = {}): Promise { let delegateDid: string | undefined; + let returnedGrants: any; if (agent === undefined) { // A custom Web5Agent implementation was not specified, so use default managed user agent. const userAgent = await Web5UserAgent.create({ agentVault }); @@ -261,6 +264,9 @@ export class Web5 { try { // TEMPORARY: Placeholder for WalletConnect integration const { connectedDid, delegateDid, delegateGrants } = await WalletConnect.initClient(walletConnectOptions); + returnedGrants = delegateGrants; + console.log('DELEGATEGRANTS ARE: '); + console.log(delegateGrants); // Import the delegated DID as an Identity in the User Agent. // Setting the connectedDID in the metadata applies a relationship between the signer identity and the one it is impersonating. @@ -383,7 +389,7 @@ export class Web5 { const web5 = new Web5({ agent, connectedDid, delegateDid }); - return { web5, did: connectedDid, delegateDid, recoveryPhrase }; + return { web5, did: connectedDid, delegateDid, recoveryPhrase, delegateGrants: returnedGrants }; } /**