Skip to content

Commit

Permalink
feat: add presentation callback parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
genaris committed Dec 12, 2024
1 parent 27c0a5f commit 88e988d
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 8 deletions.
39 changes: 37 additions & 2 deletions doc/service-agent-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand All @@ -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=<uuid>)

#### 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export class InvitationController {
example: {
summary: 'Phone Number',
value: {
ref: '1234-5678',
callbackUrl: 'https://myhost/mycallbackurl',
requestedCredentials: [
{
credentialDefinitionId:
Expand All @@ -48,7 +50,7 @@ export class InvitationController {
): Promise<CreatePresentationRequestResult> {
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')
Expand Down Expand Up @@ -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])
Expand Down
4 changes: 4 additions & 0 deletions packages/main/src/controllers/invitation/InvitationDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
}
Expand Down
37 changes: 37 additions & 0 deletions packages/main/src/events/CallbackEvent.ts
Original file line number Diff line number Diff line change
@@ -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,
})
}
}
45 changes: 41 additions & 4 deletions packages/main/src/events/MessageEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}),
Expand All @@ -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<string, PresentationStatus> = {
'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}`)
Expand Down Expand Up @@ -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({
Expand All @@ -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}`)
}
}
})
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Expand Down

0 comments on commit 88e988d

Please sign in to comment.