From 88e988d18fc709f36205b7f6e2843037e44c2ad3 Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Thu, 12 Dec 2024 19:43:00 -0300 Subject: [PATCH] feat: add presentation callback parameters --- doc/service-agent-api.md | 39 +++++++++++++++- .../invitation/InvitationController.ts | 5 ++- .../controllers/invitation/InvitationDto.ts | 4 ++ packages/main/src/events/CallbackEvent.ts | 37 +++++++++++++++ packages/main/src/events/MessageEvents.ts | 45 +++++++++++++++++-- yarn.lock | 2 +- 6 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 packages/main/src/events/CallbackEvent.ts diff --git a/doc/service-agent-api.md b/doc/service-agent-api.md index 3486bec..a53fe7e 100644 --- a/doc/service-agent-api.md +++ b/doc/service-agent-api.md @@ -51,6 +51,12 @@ In addition, it supports a notification mechanism to subscribe to any event the - [Message State Updated](#message-state-updated) - [Message Received](#message-received) - [Subscribing to events](#subscribing-to-events) + - [Invitations](#invitations) + - [Connection Invitation](#connection-invitation) + - [Presentation Request](#presentation-request) + - [Presentation Callback API](#presentation-callback-api) + - [Credential Offer](#credential-offer) + - [Presentations](#presentations) - [Verifiable Data Registry Operations](#verifiable-data-registry-operations) - [Create Credential Type](#create-credential-type) - [Initial Service Agent API Use Cases](#initial-service-agent-api-use-cases) @@ -620,7 +626,7 @@ When a Verifiable Credential Presentation is submitted, the following fields may - verified: boolean determining if the presentation is cryptographically valid - errorCode: if any, it indicated that an error has ocurred in the flow. Known error codes are the following: - 'Request declined': user has refused to present credential - - 'e.msg.no-compatble-credentials': user does not have a compatible credential to present + - 'e.msg.no-compatible-credentials': user does not have a compatible credential to present ##### Result value @@ -740,12 +746,16 @@ Note that the following Service Agent configuration environment variables are us Presentation Request invitation codes are created by specifying details of the credentials required. -This means that a single presentation request can ask for a number of attributes present in for two or more credentials. At the moment, credential requirements are only filtered by their `credentialDefinitionId`. If no `attributes` are specified, then Service Agent will ask for all attributes in the credential. +This means that a single presentation request can ask for a number of attributes present in a credential a holder might possess. +At the moment, credential requirements are only filtered by their `credentialDefinitionId`. If no `attributes` are specified, +then Service Agent will ask for all attributes in the credential. It's a POST to `/invitation/presentation-request` which receives a JSON object in the body ```json { + "callbackUrl": "https://myhost.com/presentation_callback ", + "ref": "1234-5678", "requestedCredentials": [ { "credentialDefinitionId": "full credential definition identifier", @@ -755,6 +765,10 @@ It's a POST to `/invitation/presentation-request` which receives a JSON object i } ``` +`callbackUrl` is an URL that will be called by Service Agent when the flow completes. The request follows the [Presentation Callback API](#presentation-callback-api). + +`ref` is an optional, arbitrary string that will be included in the body of the request to the callback URL. + Response will include the invitation code in both short and long form URL format. ```json @@ -772,6 +786,27 @@ Note that the following Service Agent configuration environment variables are us - AGENT_LABEL: An optional label to show along the connection invitation - PUBLIC_API_BASE_URL: Base URL for short URL creation (resulting something like https://myHost.com/s?id=) +#### Presentation Callback API + +When the presentation flow is completed (either successfully or not), Service Agent calls its `callbackUrl` as an HTTP POST with the following body: + +```json +{ + "ref": "1234-5678", + "presentationRequestId": "unique identifier for the flow", + "status": PresentationStatus, + "claims": [ { "name": "attribute-1", "value": "value-1" }, { "name": "attribute-2", "value": "value-2" }] +} +``` + +Possible values for PresentationStatus are: + +- 'ok' +- 'refused' +- 'no-compatible-credentials' +- 'verification-error' +- 'unspecified-error' + ### Credential Offer diff --git a/packages/main/src/controllers/invitation/InvitationController.ts b/packages/main/src/controllers/invitation/InvitationController.ts index 5128c03..cec0327 100644 --- a/packages/main/src/controllers/invitation/InvitationController.ts +++ b/packages/main/src/controllers/invitation/InvitationController.ts @@ -32,6 +32,8 @@ export class InvitationController { example: { summary: 'Phone Number', value: { + ref: '1234-5678', + callbackUrl: 'https://myhost/mycallbackurl', requestedCredentials: [ { credentialDefinitionId: @@ -48,7 +50,7 @@ export class InvitationController { ): Promise { const agent = await this.agentService.getAgent() - const { requestedCredentials } = options + const { requestedCredentials, ref, callbackUrl } = options if (!requestedCredentials?.length) { throw Error('You must specify a least a requested credential') @@ -104,6 +106,7 @@ export class InvitationController { }) request.proofRecord.metadata.set('_2060/requestedCredentials', requestedCredentials) + request.proofRecord.metadata.set('_2060/callbackParameters', { ref, callbackUrl }) await agent.proofs.update(request.proofRecord) const { url } = await createInvitation(await this.agentService.getAgent(), [request.message]) diff --git a/packages/main/src/controllers/invitation/InvitationDto.ts b/packages/main/src/controllers/invitation/InvitationDto.ts index 7631b64..c39bbd6 100644 --- a/packages/main/src/controllers/invitation/InvitationDto.ts +++ b/packages/main/src/controllers/invitation/InvitationDto.ts @@ -12,6 +12,10 @@ export class CreatePresentationRequestDto implements CreatePresentationRequestOp description: 'Requested credentials', example: '[{ credentialDefinitionId: "myCredentialDefinition", attributes: ["name","age"] }]', }) + ref?: string + + callbackUrl?: string + @IsNotEmpty() requestedCredentials!: RequestedCredential[] } diff --git a/packages/main/src/events/CallbackEvent.ts b/packages/main/src/events/CallbackEvent.ts new file mode 100644 index 0000000..8e21e47 --- /dev/null +++ b/packages/main/src/events/CallbackEvent.ts @@ -0,0 +1,37 @@ +import { Claim } from '@2060.io/service-agent-model' +import fetch from 'node-fetch' + +import { TsLogger } from '../utils/logger' + +export enum PresentationStatus { + OK = 'ok', + REFUSED = 'refused', + NO_COMPATIBLE_CREDENTIALS = 'no-compatible-credentials', + VERIFICATION_ERROR = 'verification-error', + UNSPECIFIED_ERROR = 'unspecified-error', +} + +export const sendPresentationCallbackEvent = async (options: { + callbackUrl: string + status: PresentationStatus + ref?: string + claims?: Claim[] + logger: TsLogger +}) => { + const { callbackUrl, ref, claims, logger } = options + try { + logger.debug(`sending presentation callback event to ${callbackUrl}: ${JSON.stringify(options)}`) + await fetch(callbackUrl, { + method: 'POST', + body: JSON.stringify({ + ref, + claims, + }), + headers: { 'Content-Type': 'application/json' }, + }) + } catch (error) { + logger.error(`Error sending presentation callback event to ${callbackUrl}`, { + cause: error, + }) + } +} diff --git a/packages/main/src/events/MessageEvents.ts b/packages/main/src/events/MessageEvents.ts index 232c7b4..53cbb8a 100644 --- a/packages/main/src/events/MessageEvents.ts +++ b/packages/main/src/events/MessageEvents.ts @@ -67,6 +67,7 @@ import { ReceiptsEventTypes } from 'credo-ts-receipts' import { ServiceAgent } from '../utils/ServiceAgent' import { createDataUrl } from '../utils/parsers' +import { PresentationStatus, sendPresentationCallbackEvent } from './CallbackEvent' import { sendWebhookEvent } from './WebhookEvent' // FIXME: timestamps are currently taken from reception date. They should be get from the originating DIDComm message @@ -206,12 +207,14 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = config.logger.info('Presentation problem report received') try { const record = await agent.proofs.getByThreadAndConnectionId(message.threadId, connection.id) + const errorCode = + (message as V2PresentationProblemReportMessage).description.en ?? + (message as V2PresentationProblemReportMessage).description.code + const msg = new IdentityProofSubmitMessage({ submittedProofItems: [ new VerifiableCredentialSubmittedProofItem({ - errorCode: - (message as V2PresentationProblemReportMessage).description.en ?? - (message as V2PresentationProblemReportMessage).description.code, + errorCode, id: record.threadId, // TODO: store id as a tag proofExchangeId: record.id, }), @@ -222,6 +225,25 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = timestamp: record.updatedAt, }) + // Call callbackUrl if existant. Depending on the received error code, set status + const callbackParameters = record.metadata.get('_2060/callbackParameters') as + | { ref?: string; callbackUrl?: string } + | undefined + + if (callbackParameters && callbackParameters.callbackUrl) { + const errorMap: Record = { + 'Request declined': PresentationStatus.REFUSED, + 'e.msg.no-compatible-credentials': PresentationStatus.NO_COMPATIBLE_CREDENTIALS, + } + + await sendPresentationCallbackEvent({ + callbackUrl: callbackParameters.callbackUrl, + status: errorMap[errorCode] ?? PresentationStatus.UNSPECIFIED_ERROR, + logger: config.logger, + ref: callbackParameters.ref, + }) + } + await sendMessageReceivedEvent(agent, msg, msg.timestamp, config) } catch (error) { config.logger.error(`Error processing presentaion problem report: ${error}`) @@ -262,6 +284,21 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = } } } + + // Call callbackUrl if existant. Depending on the received error code, set status + const callbackParameters = record.metadata.get('_2060/callbackParameters') as + | { ref?: string; callbackUrl?: string } + | undefined + + if (callbackParameters && callbackParameters.callbackUrl) { + await sendPresentationCallbackEvent({ + callbackUrl: callbackParameters.callbackUrl, + status: record.isVerified ? PresentationStatus.OK : PresentationStatus.VERIFICATION_ERROR, + logger: config.logger, + ref: callbackParameters.ref, + }) + } + const msg = new IdentityProofSubmitMessage({ submittedProofItems: [ new VerifiableCredentialSubmittedProofItem({ @@ -279,7 +316,7 @@ export const messageEvents = async (agent: ServiceAgent, config: ServerConfig) = await sendMessageReceivedEvent(agent, msg, msg.timestamp, config) } catch (error) { - config.logger.error(`Error processing presentaion message: ${error}`) + config.logger.error(`Error processing presentation message: ${error}`) } } }) diff --git a/yarn.lock b/yarn.lock index e3e7421..2809295 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1986,7 +1986,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^20.11.19": +"@types/node@*", "@types/node@20.11.19", "@types/node@^20.11.19": version "20.11.19" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195" integrity sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==