From 1288e3773cdc9219755fe1fee339d59e695a485c Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Thu, 3 Oct 2024 15:35:57 -0700 Subject: [PATCH] Added web5 connect client/app display name for dynamic rendering in the wallet --- packages/agent/src/connect.ts | 11 +++++++++-- packages/agent/src/oidc.ts | 25 +++++++++++++++++++++---- packages/agent/tests/connect.spec.ts | 8 ++++++++ packages/api/src/web5.ts | 8 +++++++- 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/packages/agent/src/connect.ts b/packages/agent/src/connect.ts index 68aff74e9..936ed2da1 100644 --- a/packages/agent/src/connect.ts +++ b/packages/agent/src/connect.ts @@ -17,6 +17,7 @@ import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; * a did from a provider. */ async function initClient({ + displayName, connectServerUrl, walletUri, permissionRequests, @@ -44,10 +45,12 @@ async function initClient({ const request = await Oidc.createAuthRequest({ client_id : clientDid.uri, scope : 'openid did:jwk', + redirect_uri : callbackEndpoint, + // custom properties: // code_challenge : codeChallengeBase64Url, // code_challenge_method : 'S256', permissionRequests : permissionRequests, - redirect_uri : callbackEndpoint, + displayName, }); // Sign the Request Object using the Client DID's signing key. @@ -91,6 +94,7 @@ async function initClient({ // a deeplink to a web5 compatible wallet. if the wallet scans this link it should receive // a route to its web5 connect provider flow and the params of where to fetch the auth request. + console.log('Wallet URI:', walletUri); const generatedWalletUri = new URL(walletUri); generatedWalletUri.searchParams.set('request_uri', parData.request_uri); generatedWalletUri.searchParams.set( @@ -133,7 +137,10 @@ async function initClient({ * a did from a provider. */ export type WalletConnectOptions = { - /** The URL of the intermediary server which relays messages between the client and provider */ + /** The user friendly name of the client/app to be displayed when prompting end-user with permission requests. */ + displayName: string; + + /** The URL of the intermediary server which relays messages between the client and provider. */ connectServerUrl: string; /** diff --git a/packages/agent/src/oidc.ts b/packages/agent/src/oidc.ts index e56e9eb1f..60a8252e3 100644 --- a/packages/agent/src/oidc.ts +++ b/packages/agent/src/oidc.ts @@ -128,6 +128,9 @@ export type SIOPv2AuthRequest = { * The contents of this are inserted into a JWT inside of the {@link PushedAuthRequest}. */ export type Web5ConnectAuthRequest = { + /** The user friendly name of the client/app to be displayed when prompting end-user with permission requests. */ + displayName: string; + /** PermissionGrants that are to be sent to the provider */ permissionRequests: ConnectPermissionRequest[]; } & SIOPv2AuthRequest; @@ -242,7 +245,7 @@ async function generateCodeChallenge() { async function createAuthRequest( options: RequireOnly< Web5ConnectAuthRequest, - 'client_id' | 'scope' | 'redirect_uri' | 'permissionRequests' + 'client_id' | 'scope' | 'redirect_uri' | 'permissionRequests' | 'displayName' > ) { // Generate a random state value to associate the authorization request with the response. @@ -628,6 +631,7 @@ async function createPermissionGrants( 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 + console.log(`Creating permission grants for ${scopes.length} scopes given...`); const permissionGrants = await Promise.all( scopes.map((scope) => { // check if the scope is a records permission scope, or a protocol configure scope, if so it should use a delegated permission. @@ -643,6 +647,7 @@ async function createPermissionGrants( }) ); + console.log(`Sending ${permissionGrants.length} permission grants to remote DWN...`); const messagePromises = permissionGrants.map(async (grant) => { // Quirk: we have to pull out encodedData out of the message the schema validator doesn't want it there const { encodedData, ...rawMessage } = grant.message; @@ -658,6 +663,8 @@ async function createPermissionGrants( // 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) { + console.log('Error sending RecordsWrite:', reply.status.detail); + console.log('RecordsWrite message:', rawMessage); throw new Error( `Could not send the message. Error details: ${reply.status.detail}` ); @@ -666,9 +673,13 @@ async function createPermissionGrants( return grant.message; }); - const messages = await Promise.all(messagePromises); - - return messages; + try { + const messages = await Promise.all(messagePromises); + return messages; + } catch (error) { + console.error('Error during batch-send of permission grants:', error instanceof Error ? error.message : error); + throw error; + } } /** @@ -693,6 +704,7 @@ async function prepareProtocol( `Could not fetch protocol: ${queryMessage.reply.status.detail}` ); } else if (queryMessage.reply.entries === undefined || queryMessage.reply.entries.length === 0) { + console.log('Protocol does not exist, creating:', protocolDefinition.protocol); // send the protocol definition to the remote DWN first, if it passes we can process it locally const { reply: sendReply, message: configureMessage } = await agent.sendDwnRequest({ @@ -716,6 +728,7 @@ async function prepareProtocol( }); } else { + console.log('Protocol already exists:', protocolDefinition.protocol); // the protocol already exists, let's make sure it exists on the remote DWN as the requesting app will need it const configureMessage = queryMessage.reply.entries![0]; @@ -776,6 +789,7 @@ async function submitAuthResponse( const delegateGrants = (await Promise.all(delegateGrantPromises)).flat(); + console.log('Generating auth response object...'); const responseObject = await Oidc.createResponseObject({ //* the IDP's did that was selected to be connected iss : selectedDid, @@ -790,6 +804,7 @@ async function submitAuthResponse( }); // Sign the Response Object using the ephemeral DID's signing key. + console.log('Signing auth response object...'); const responseObjectJwt = await Oidc.signJwt({ did : delegateBearerDid, data : responseObject, @@ -801,6 +816,7 @@ async function submitAuthResponse( clientDid?.didDocument! ); + console.log('Encrypting auth response object...'); const encryptedResponse = Oidc.encryptAuthResponse({ jwt : responseObjectJwt!, encryptionKey : sharedKey, @@ -813,6 +829,7 @@ async function submitAuthResponse( state : authRequest.state, }).toString(); + console.log(`Sending auth response object to Web5 Connect server: ${authRequest.redirect_uri}`); await fetch(authRequest.redirect_uri, { body : formEncodedRequest, method : 'POST', diff --git a/packages/agent/tests/connect.spec.ts b/packages/agent/tests/connect.spec.ts index f6a1b87cb..9f34a8e10 100644 --- a/packages/agent/tests/connect.spec.ts +++ b/packages/agent/tests/connect.spec.ts @@ -224,6 +224,7 @@ describe('web5 connect', function () { }); const options = { + displayName : 'Sample App', client_id : clientEphemeralPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), @@ -457,6 +458,7 @@ describe('web5 connect', function () { fetchStub.callThrough(); const results = await WalletConnect.initClient({ + displayName : 'Sample App', walletUri : 'http://localhost:3000/', connectServerUrl : 'http://localhost:3000/connect', permissionRequests : [ @@ -505,6 +507,7 @@ describe('web5 connect', function () { }); const options = { + displayName : 'Sample App', client_id : clientEphemeralPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), @@ -560,6 +563,7 @@ describe('web5 connect', function () { }); const options = { + displayName : 'Sample App', client_id : clientEphemeralPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), @@ -632,6 +636,7 @@ describe('web5 connect', function () { }); const options = { + displayName : 'Sample App', client_id : clientEphemeralPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), @@ -679,6 +684,7 @@ describe('web5 connect', function () { }); const options = { + displayName : 'Sample App', client_id : clientEphemeralPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), @@ -730,6 +736,7 @@ describe('web5 connect', function () { }); const options = { + displayName : 'Sample App', client_id : clientEphemeralPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), @@ -781,6 +788,7 @@ describe('web5 connect', function () { mismatchedScopes[0].protocol = 'http://profile-protocol.xyz/other'; const options = { + displayName : 'Sample App', client_id : clientEphemeralPortableDid.uri, scope : 'openid did:jwk', // code_challenge : Convert.uint8Array(codeChallenge).toBase64Url(), diff --git a/packages/api/src/web5.ts b/packages/api/src/web5.ts index 6636026f5..0eca66de4 100644 --- a/packages/api/src/web5.ts +++ b/packages/api/src/web5.ts @@ -44,6 +44,7 @@ export type ConnectPermissionRequest = { * The protocol definition for the protocol being requested. */ protocolDefinition: DwnProtocolDefinition; + /** * The permissions being requested for the protocol. If none are provided, the default is to request all permissions. */ @@ -51,9 +52,14 @@ export type ConnectPermissionRequest = { } /** - * Options for connecting to a Web5 agent. This includes the ability to connect to an external wallet + * Options for connecting to a Web5 agent. This includes the ability to connect to an external wallet. + * + * NOTE: the returned `ConnectPermissionRequest` type is different to the `ConnectPermissionRequest` type in the `@web5/agent` package. */ export type ConnectOptions = Omit & { + /** The user friendly name of the client/app to be displayed when prompting end-user with permission requests. */ + displayName: string; + /** * The permissions that are being requested for the connected DID. * This is used to create the {@link ConnectPermissionRequest} for the wallet connect flow.