diff --git a/docs/examples/ConnectionlessOffer.md b/docs/examples/ConnectionlessOffer.md index 66881bd69..5dc7f956f 100644 --- a/docs/examples/ConnectionlessOffer.md +++ b/docs/examples/ConnectionlessOffer.md @@ -1,28 +1,157 @@ # Edge SDK Connectionless Credential Offer -## Flow -1. Obtain a Connectionless Credential Offer from an Issuer. +## User Flow -A Connectionless Credential Offer is an Out of Band Invitation with a Credential Offer Attachment. -This should be a URI with a single query parameter `_oob`, which is an encoded JSON. -It should look similar to: +```mermaid +sequenceDiagram + autonumber -``` -https://my.domain.com/path?_oob=eyJpZCI6ImY5NmUzNjk5LTU5MWMtNGFlNy1iNWU2LTZlZmU2ZDI2MjU1YiIsInR5cGUiOiJodHRwczovL2RpZGNvbW0ub3JnL291dC1vZi1iYW5kLzIuMC9pbnZpdGF0aW9uIiwiZnJvbSI6ImRpZDpwZWVyOjIuRXo2TFNmc0tNZTh2U1NXa1lkWkNwbjRZVmlQRVJmZEdBaGRMQUdIZ3gyTEdKd2ZtQS5WejZNa3B3MWtTYWJCTXprQTN2NTl0UUZuaDNGdGtLeTZ4TGhMeGQ5UzZCQW9hQmcyLlNleUowSWpvaVpHMGlMQ0p6SWpwN0luVnlhU0k2SW1oMGRIQTZMeTh4T1RJdU1UWTRMakV1TXpjNk9EQTRNQzlrYVdSamIyMXRJaXdpY2lJNlcxMHNJbUVpT2xzaVpHbGtZMjl0YlM5Mk1pSmRmWDAiLCJib2R5Ijp7ImdvYWxfY29kZSI6Imlzc3VlLXZjIiwiZ29hbCI6IlRlc3QgT09CIiwiYWNjZXB0IjpbImRpZGNvbW0vdjIiXX0sImNyZWF0ZWRfdGltZSI6MTcyNDg1MTEzOSwiZXhwaXJlc190aW1lIjo5OTI0ODUxNDM5LCJhdHRhY2htZW50cyI6W3siaWQiOiIwMGNkYzkwYy05YTk5LTRjZGEtODdmZS00ZjRiMjU5NTExMmEiLCJtZWRpYV90eXBlIjoiYXBwbGljYXRpb24vanNvbiIsImRhdGEiOnsianNvbiI6eyJpZCI6IjY1NWU5YTJjLTQ4ZWQtNDU5Yi1iM2RhLTZiMzY4NjY1NTU2NCIsInR5cGUiOiJodHRwczovL2RpZGNvbW0ub3JnL2lzc3VlLWNyZWRlbnRpYWwvMy4wL29mZmVyLWNyZWRlbnRpYWwiLCJib2R5Ijp7ImdvYWxfY29kZSI6Ik9mZmVyIENyZWRlbnRpYWwiLCJjcmVkZW50aWFsX3ByZXZpZXciOnsidHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvaXNzdWUtY3JlZGVudGlhbC8zLjAvY3JlZGVudGlhbC1jcmVkZW50aWFsIiwiYm9keSI6eyJhdHRyaWJ1dGVzIjpbeyJuYW1lIjoiZmFtaWx5TmFtZSIsInZhbHVlIjoiV29uZGVybGFuZCJ9XX19fSwiYXR0YWNobWVudHMiOlt7ImlkIjoiODQwNDY3OGItOWEzNi00OTg5LWFmMWQtMGY0NDUzNDdlMGUzIiwibWVkaWFfdHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJkYXRhIjp7Impzb24iOnsib3B0aW9ucyI6eyJjaGFsbGVuZ2UiOiJhZDBmNDNhZC04NTM4LTQxZDQtOWNiOC0yMDk2N2JjNjg1YmMiLCJkb21haW4iOiJkb21haW4ifSwicHJlc2VudGF0aW9uX2RlZmluaXRpb24iOnsiaWQiOiI3NDhlZmE1OC0yYmNlLTQ0MGQtOTIxZi0yNTIwYTg0NDY2NjMiLCJpbnB1dF9kZXNjcmlwdG9ycyI6W10sImZvcm1hdCI6eyJqd3QiOnsiYWxnIjpbIkVTMjU2SyJdLCJwcm9vZl90eXBlIjpbXX19fX19LCJmb3JtYXQiOiJwcmlzbS9qd3QifV0sInRoaWQiOiJmOTZlMzY5OS01OTFjLTRhZTctYjVlNi02ZWZlNmQyNjI1NWIiLCJmcm9tIjoiZGlkOnBlZXI6Mi5FejZMU2ZzS01lOHZTU1drWWRaQ3BuNFlWaVBFUmZkR0FoZExBR0hneDJMR0p3Zm1BLlZ6Nk1rcHcxa1NhYkJNemtBM3Y1OXRRRm5oM0Z0a0t5NnhMaEx4ZDlTNkJBb2FCZzIuU2V5SjBJam9pWkcwaUxDSnpJanA3SW5WeWFTSTZJbWgwZEhBNkx5OHhPVEl1TVRZNExqRXVNemM2T0RBNE1DOWthV1JqYjIxdElpd2ljaUk2VzEwc0ltRWlPbHNpWkdsa1kyOXRiUzkyTWlKZGZYMCJ9fX1dfQ== -``` + actor Peer + actor Admin + participant Issuer + Admin->> Issuer: Request a Connectionless Credential Offer Invitation + Issuer->> Admin: Connectionless Credential Offer Invitation -2. Ensure the validity of the Invitation with `Agent.parseInvitation` + Admin->>Peer: Connectionless Credential Offer Invitation (OOB) -`parseInvitation` decodes and validates the encoded Out of Band Invitation, plus attachments, returning an instance of `OutOfBandInvitation` on success. This OutOfBandInvitation will have a single Attachment for the Credential Offer. + Peer ->> Peer: Parse the OOB + + Peer ->> Issuer: Accept the Connectionless Credential Offer Invitation (OOB) + Issuer ->> Peer: Connectionless Credential Offer + Peer ->> Issuer: Accept the Connectionless Credential Offer + Issuer ->> Peer: Verifiable Credential -``` -const oob = await Agent.parseInvitation(rawOob); ``` -3. Use `Agent.acceptInvitation` to handle the OutOfBandInvitation appropriately. -In this case, with an attached Credential Offer, the Credential Offer Message will be stored in Pluto. +## Steps -``` -await Agent.acceptInvitation(oob) -``` +1. The `Admin` creates a Credential Offer as Invitation for connectionless issuance Issuer Agent + + ```bash + curl --location --request POST 'http://localhost:8000/cloud-agent/issue-credentials/credential-offers/invitation' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "claims": { + "emailAddress": "sampleEmail", + "familyName": "", + "dateOfIssuance": "2023-01-01T02:02:02Z", + "drivingLicenseID": "", + "drivingClass": 1 + }, + "goalCode": "issue-vc", + "goal": "Request issuance", + "credentialFormat": "JWT", + "issuingDID": [[publishedPrismDID]], + "automaticIssuance": true + }' + + ``` + +2. The Issuer sends back the `Connectionless Credential Offer Invitation` + - The response should like something like this: + + ```json + { + "recordId": "cf11283a-a9f5-4da0-ae65-be1e57a40710", + "thid": "b78d7221-0149-4c29-a42e-d8b8c4a9f57d", + "credentialFormat": "JWT", + "claims": { + "emailAddress": "sampleEmail", + "familyName": "", + "dateOfIssuance": "2023-01-01T02:02:02Z", + "drivingLicenseID": "", + "drivingClass": 1 + }, + "automaticIssuance": true, + "createdAt": "2024-10-15T01:31:55.303311774Z", + "role": "Issuer", + "protocolState": "InvitationGenerated", + "goalCode": "issue-vc", + "goal": "Request issuance", + "myDid": "did:peer:2.Ez6LS...", + "invitation": { + "id": "b78d7221-0149-4c29-a42e-d8b8c4a9f57d", + "type": "https://didcomm.org/out-of-band/2.0/invitation", + "from": "did:peer:2.Ez6LS...", + "invitationUrl": "https://my.domain.com/path?_oob=eyJpZCI6ImI3OGQ3MjIxLTAxNDktNGMyOS1hNDJlLWQ4YjhjNGE5ZjU3ZCIsInR5cGUiOiJodHRwczovL2RpZGNvbW0ub3JnL291dC1vZi1iYW5kLzIuMC9pbnZpdGF0aW9uIiwiZnJvbSI6ImRpZDpwZWVyOjIuRXo2TFNnbTg4eW9jTWFMNkQxM203NWdHcDNlUVRQMXZCbjZpRTRDR0d6UnNhTUtOUC5WejZNa3JVaG40UlBCdHRWRXNXWGI1MlFOOWZUOW1LYmV3ZzhRQnBBOE5GOEZxUm9CLlNleUowSWpvaVpHMGlMQ0p6SWpwN0luVnlhU0k2SW1oMGRIQTZMeTh4T1RJdU1UWTRMalk0TGpZeU9qZ3dNREF2Wkdsa1kyOXRiU0lzSW5JaU9sdGRMQ0poSWpwYkltUnBaR052YlcwdmRqSWlYWDE5IiwiYm9keSI6eyJnb2FsX2NvZGUiOiJpc3N1ZS12YyIsImdvYWwiOiJSZXF1ZXN0IGlzc3VhbmNlIiwiYWNjZXB0IjpbImRpZGNvbW0vdjIiXX0sImF0dGFjaG1lbnRzIjpbeyJpZCI6IjdiODM3ZTk3LTYzNzktNDJkNy1hNjY4LTA3OTAzYTFlYmIwNCIsIm1lZGlhX3R5cGUiOiJhcHBsaWNhdGlvbi9qc29uIiwiZGF0YSI6eyJqc29uIjp7ImlkIjoiN2VhMmJlZTMtODgxNC00YjhlLTllNGEtNWZhMDU1YWJiZDUwIiwidHlwZSI6Imh0dHBzOi8vZGlkY29tbS5vcmcvaXNzdWUtY3JlZGVudGlhbC8zLjAvb2ZmZXItY3JlZGVudGlhbCIsImJvZHkiOnsiZ29hbF9jb2RlIjoiT2ZmZXIgQ3JlZGVudGlhbCIsImNyZWRlbnRpYWxfcHJldmlldyI6eyJ0eXBlIjoiaHR0cHM6Ly9kaWRjb21tLm9yZy9pc3N1ZS1jcmVkZW50aWFsLzMuMC9jcmVkZW50aWFsLWNyZWRlbnRpYWwiLCJib2R5Ijp7ImF0dHJpYnV0ZXMiOlt7Im5hbWUiOiJtZW1iZXJPZiIsInZhbHVlIjoiZXlKMGNtbGlaVWxrSWpvaUpYa3hhVkZWZEhWSk5uZEZNMFUyWmpneWNpODFPRTl1U1ZGcE4zTnROMWx4VVVFellrZHNWVUowWTI4OUxtTnNiMkZyWldRaUxDSjBjbWxpWlU1aGJXVWlPaUpYYUdGdVoyRnliMkVnVUdGd1lTQklZWEIxSW4wPSIsIm1lZGlhX3R5cGUiOiJhcHBsaWNhdGlvbi9qc29uIn0seyJuYW1lIjoicGVyc29uIiwidmFsdWUiOiJleUptZFd4c1RtRnRaU0k2SWtKbGJpQlVZV2x5WldFaUxDSmtZWFJsVDJaQ2FYSjBhQ0k2SWpFNU9EY3ZXRmd2V0ZnaWZRPT0iLCJtZWRpYV90eXBlIjoiYXBwbGljYXRpb24vanNvbiJ9XX19fSwiYXR0YWNobWVudHMiOlt7ImlkIjoiODJkMjI1NDctY2VmNS00YjkwLTg5MTQtOGQzY2M2ZGM2MjFlIiwibWVkaWFfdHlwZSI6ImFwcGxpY2F0aW9uL2pzb24iLCJkYXRhIjp7Impzb24iOnsib3B0aW9ucyI6eyJjaGFsbGVuZ2UiOiI5Y2Y3ZmE4OC05MGM5LTQyZTMtOTI1MC0xOGZjMzZhYjkzYWMiLCJkb21haW4iOiJkb21haW4ifSwicHJlc2VudGF0aW9uX2RlZmluaXRpb24iOnsiaWQiOiI5YTJhYjgyNi1iOWZjLTQ3ZjEtOGM0Mi1hMGFhMjI1ZjQwMzAiLCJpbnB1dF9kZXNjcmlwdG9ycyI6W10sImZvcm1hdCI6eyJqd3QiOnsiYWxnIjpbIkVTMjU2SyJdfX19fX0sImZvcm1hdCI6InByaXNtL2p3dCJ9XSwidGhpZCI6ImI3OGQ3MjIxLTAxNDktNGMyOS1hNDJlLWQ4YjhjNGE5ZjU3ZCIsImZyb20iOiJkaWQ6cGVlcjoyLkV6NkxTZ204OHlvY01hTDZEMTNtNzVnR3AzZVFUUDF2Qm42aUU0Q0dHelJzYU1LTlAuVno2TWtyVWhuNFJQQnR0VkVzV1hiNTJRTjlmVDltS2Jld2c4UUJwQThORjhGcVJvQi5TZXlKMElqb2laRzBpTENKeklqcDdJblZ5YVNJNkltaDBkSEE2THk4eE9USXVNVFk0TGpZNExqWXlPamd3TURBdlpHbGtZMjl0YlNJc0luSWlPbHRkTENKaElqcGJJbVJwWkdOdmJXMHZkaklpWFgxOSJ9fX1dLCJjcmVhdGVkX3RpbWUiOjE3Mjg5NTU5MTUsImV4cGlyZXNfdGltZSI6MTcyODk1NjIxNX0=" + }, + "metaRetries": 5 + } + ``` + +3. The `Admin` forwards the `invitationUrl` on to the `Peer` + +4. The `Peer` checks the validity of the `invitationUrl` using `agent.parseInvitation` + + - `parseInvitation` decodes and validates the encoded Out of Band Invitation, plus attachments, returning an instance of `OutOfBandInvitation` on success. This OutOfBandInvitation will have a single Attachment for the Credential Offer. + + ```typescript + const oob = await agent.parseInvitation(invitationUrl) + ``` + +5. The `Peer` accepts the Invitation using `agent.acceptInvitation` + + - In this case, with an attached Credential Offer, the Credential Offer Message will be stored in Pluto. + + ```typescript + await agent.acceptInvitation(oob) + ``` + +6. The `Issuer` will then send back the credential offer. +7. The `Peer` listens for the credential offer response from the `Issuer`, accepts it + + ```typescript + agent.addListener(SDK.ListenerKey.MESSAGE, async (newMessages: SDK.Domain.Message[]) => { + // newMessages can contain any didcomm message that is received, including + // Credential Offers, Issued credentials and Request Presentation Messages + const credentialOffers = newMessages.filter( + (message) => message.piuri === 'https://didcomm.org/issue-credential/3.0/offer-credential' + ) + + if (credentialOffers.length) { + for (const credentialOfferMessage of credentialOffers) { + try { + // Create credential offer object from the message + const credentialOffer = await SDK.OfferCredential.fromMessage(credentialOfferMessage) + + // Prepare the credential request + const requestCredential = await agent.prepareRequestCredentialWithIssuer(credentialOffer) + + // Send the credential request + await agent.sendMessage(requestCredential.makeMessage()) + } catch (err) { + console.error('Error occurred while sending the credential request:', err) + } + } + } + }) + ``` + +8. The `Issuer` sends back the `Verifiable Credential` + + - The `Peer`listens for the credential and stores it in pluto + + ```javascript + agent.addListener(SDK.ListenerKey.MESSAGE, async (newMessages: SDK.Domain.Message[]) => { + // newMessages can contain any didcomm message that is received, including + // Credential Offers, Issued credentials and Request Presentation Messages + const issuedCredentials = newMessages.filter( + (message) => message.piuri === 'https://didcomm.org/issue-credential/3.0/issue-credential' + ) + + if (issuedCredentials.length) { + for (const issuedCredential of issuedCredentials) { + try { + // Create issue credential object from the message + const issueCredential = await SDK.IssueCredential.fromMessage(issuedCredential) + + // Store the credential in pluto + await agent.processIssuedCredentialMessage(issueCredential) + } catch (err) { + console.error('Error occurred while storing the credential:', err) + } + } + } + }) + ```