From ac2110eb158e5969b4290436743ca07593955c43 Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Thu, 3 Apr 2025 13:20:53 +0530 Subject: [PATCH 1/4] feat: Add support for jsonLd issuance --- packages/credo/src/agent.ts | 3 +- packages/credo/src/index.ts | 1 + packages/credo/src/tools/credential.ts | 124 +++++++++++++++++++++++-- packages/credo/src/tools/did.ts | 6 +- packages/credo/src/types.ts | 101 ++++++++++++++------ 5 files changed, 195 insertions(+), 40 deletions(-) diff --git a/packages/credo/src/agent.ts b/packages/credo/src/agent.ts index 3f21164..df7c03f 100644 --- a/packages/credo/src/agent.ts +++ b/packages/credo/src/agent.ts @@ -8,6 +8,7 @@ import { CredentialsModule, DidsModule, HttpOutboundTransport, + JsonLdCredentialFormatService, ProofsModule, V2CredentialProtocol, V2ProofProtocol, @@ -132,7 +133,7 @@ function getAskarAnonCredsModules(mnemonic: string) { autoAcceptCredentials: AutoAcceptCredential.Always, credentialProtocols: [ new V2CredentialProtocol({ - credentialFormats: [new AnonCredsCredentialFormatService()], + credentialFormats: [new AnonCredsCredentialFormatService(), new JsonLdCredentialFormatService()], }), ], }), diff --git a/packages/credo/src/index.ts b/packages/credo/src/index.ts index 6a35616..f7c7654 100644 --- a/packages/credo/src/index.ts +++ b/packages/credo/src/index.ts @@ -61,6 +61,7 @@ export class CredoToolKit { new CredentialToolHandler(this.credo).listCredentialsTool(), new CredentialToolHandler(this.credo).getCredentialRecordTool(), new CredentialToolHandler(this.credo).listCredentialExchangeRecordsTool(), + new CredentialToolHandler(this.credo).acceptCredentialRequestTool(), new ProofToolHandler(this.credo).connectionlessProofRequestTool(), new ProofToolHandler(this.credo).connectionProofRequestTool(), new ProofToolHandler(this.credo).getProofRecordTool(), diff --git a/packages/credo/src/tools/credential.ts b/packages/credo/src/tools/credential.ts index 726929b..bcd0f21 100644 --- a/packages/credo/src/tools/credential.ts +++ b/packages/credo/src/tools/credential.ts @@ -1,13 +1,21 @@ -import { AutoAcceptCredential, V2CredentialPreview } from '@credo-ts/core'; +import { + AutoAcceptCredential, + CredentialFormatPayload, + JsonLdCredentialDetailFormat, + V2CredentialPreview, + W3cJsonLdVerifiableCredentialOptions, +} from '@credo-ts/core'; import QRCode from 'qrcode'; import { CredoAgent } from '../agent.js'; import { AcceptCredentialOfferParams, ConnectionlessCredentialOfferParams, - CredentialOfferParams, GetCredentialRecordParams, ListCredentialParams, ToolDefinition, + CredentialOfferParams, + VC_CONTEXT, + VC_TYPE, } from '../types.js'; /** @@ -88,15 +96,87 @@ export class CredentialToolHandler { description: 'Creates a credential offer for an existing DIDComm connection. The offer is automatically sent to the connected agent, who can accept it to receive the credential. Returns the credential record with details about the offer status.', schema: CredentialOfferParams, - handler: async ({ attributes, credentialDefinitionId, connectionId }) => { - let credentialRecord = await this.credo.agent.credentials.offerCredential({ - comment: 'V2 Out of Band offer', - credentialFormats: { + handler: async ({ anoncreds, jsonld, connectionId }) => { + let credentialFormats: CredentialFormatPayload; + if (anoncreds) { + credentialFormats = { anoncreds: { - attributes: V2CredentialPreview.fromRecord(attributes).attributes, - credentialDefinitionId, + attributes: V2CredentialPreview.fromRecord(anoncreds.attributes).attributes, + credentialDefinitionId: anoncreds.credentialDefinitionId, }, - }, + }; + } else if (jsonld) { + const { + attributes, + credentialName, + credentialSummary, + credentialSchema, + issuerDid, + subjectDid, + type, + expirationDate, + credentialStatus, + context, + format, + connector, + credentialId, + ...additionalData + } = jsonld; + + // validate issuer DID + const didDocument = await this.credo.agent.dids.resolveDidDocument(issuerDid); + if (!didDocument || !didDocument.assertionMethod) { + return { + content: [ + { + type: 'text', + text: `Issuer DID ${issuerDid} does not exist with an assertionMethod`, + }, + ], + }; + } + + credentialFormats = { + jsonld: { + credential: { + '@context': [...(context || []), ...VC_CONTEXT], + type: [...(type || []), VC_TYPE], + credentialSubject: { + id: subjectDid, + ...attributes, + }, + issuer: issuerDid, + issuanceDate: new Date().toDateString(), + expirationDate: + expirationDate instanceof Date ? expirationDate.toISOString() : expirationDate, + credentialStatus: credentialStatus + ? { + id: credentialStatus.statusListName, + type: credentialStatus.statusPurpose, + } + : undefined, + ...additionalData, + }, + options: { + proofType: 'Ed25519Signature2018', // validate supported types + proofPurpose: 'assertionMethod', + }, + } satisfies JsonLdCredentialDetailFormat, + }; + } else { + return { + content: [ + { + type: 'text', + text: 'Error credential format jsonld, anoncreds with arguments must be provided', + }, + ], + }; + } + + let credentialRecord = await this.credo.agent.credentials.offerCredential({ + comment: 'V2 Out of Band offer', + credentialFormats: credentialFormats, protocolVersion: 'v2', connectionId, autoAcceptCredential: AutoAcceptCredential.Always, @@ -215,4 +295,30 @@ export class CredentialToolHandler { }, }; } + + /** + * Accepts a credential request from an holder to continue credential issuance. + * Completes the credential exchange process and returns the credential record. + */ + acceptCredentialRequestTool(): ToolDefinition { + return { + name: 'accept-credential-request', + description: 'Accepts a credential request from an holder using the provided credential record ID.', + schema: AcceptCredentialOfferParams, + handler: async ({ credentialRecordId }) => { + const credential = await this.credo.agent.credentials.acceptRequest({ + credentialRecordId, + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(credential), + }, + ], + }; + }, + }; + } } diff --git a/packages/credo/src/tools/did.ts b/packages/credo/src/tools/did.ts index b15a7d4..16ec31a 100644 --- a/packages/credo/src/tools/did.ts +++ b/packages/credo/src/tools/did.ts @@ -56,13 +56,13 @@ export class DidToolHandler { description: 'Creates and publishes a new DID document to the specified cheqd network (testnet or mainnet). Generates a new DID with default verification methods and returns the complete DID document.', schema: CreateDidDocumentParams, - handler: async ({ network }) => { + handler: async ({ network, verificationMethodType, verificationMethodId }) => { const result = await this.credo.agent.dids.create({ method: 'cheqd', secret: { verificationMethod: { - id: 'key-1', // can be a param - type: 'Ed25519VerificationKey2020', // can be a param + id: verificationMethodId || 'key-1', + type: verificationMethodType || 'Ed25519VerificationKey2020', }, }, options: { diff --git a/packages/credo/src/types.ts b/packages/credo/src/types.ts index e1753a0..eb4a655 100644 --- a/packages/credo/src/types.ts +++ b/packages/credo/src/types.ts @@ -24,26 +24,32 @@ export interface ICredoToolKitOptions { /** * Schema for validating cheqd Decentralized Identifiers (DIDs). - * Must start with 'did:cheqd:' followed by network and unique identifier. + * CHEQD_DID Must start with 'did:cheqd:' followed by network and unique identifier. */ -const DID = z - .string() - .startsWith('did:cheqd:') - .describe( - 'A cheqd Decentralized Identifier (DID) in the format: did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009' - ); +const DID = z.string().startsWith('did:'); + +const CHEQD_DID = DID.describe( + 'A cheqd Decentralized Identifier (DID) in the format: did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009' +); + +export const VC_CONTEXT = ['https://www.w3.org/2018/credentials/v1']; +export const VC_TYPE = 'VerifiableCredential'; /** * Schema for validating DID URLs that point to resources. * Must be a valid cheqd DID followed by '/resources/'. */ -export const DID_URL = z.string().startsWith('did:cheqd:').includes('/resources/'); +export const CHEQD_DID_URL = CHEQD_DID.includes('/resources/'); // DID Document Management Parameters export const CreateDidDocumentParams = { network: z .enum(['testnet', 'mainnet']) .describe('The cheqd network to publish the DID document (testnet or mainnet)'), + verificationMethodType: z + .enum(['Ed25519VerificationKey2018', 'Ed25519VerificationKey2020', 'JsonWebKey2020']) + .optional(), + verificationMethodId: z.string().optional(), }; /** @@ -93,25 +99,25 @@ const DidDocumentSchema = z.object({ }); export const UpdateDidDocumentParams = { - did: DID, + did: CHEQD_DID, options: z.record(z.any()).optional(), didDocument: DidDocumentSchema, }; export const DeactivateDidDocumentParams = { - did: DID, + did: CHEQD_DID, }; export const ResolveDidDocumentParams = { - did: DID, + did: CHEQD_DID, }; export const ResolveDidLinkedResourceParams = { - didUrl: DID, + didUrl: CHEQD_DID, }; export const CreateDidLinkedResourceParams = { - did: DID, + did: CHEQD_DID, id: z.string().uuid(), name: z.string(), resourceType: z.string(), @@ -130,7 +136,7 @@ export const CreateDidLinkedResourceParams = { // Anonymous Credentials Parameters export const ResolveSchemaIdParams = { - schemaId: DID_URL.describe( + schemaId: CHEQD_DID_URL.describe( 'The DID URL of the schema to resolve, e.g., did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009/resources/5acb3d53-ba06-441a-b48b-07d8c2f129f8' ), }; @@ -153,15 +159,15 @@ export const RegisterSchemaParams = { export const ListSchemaParams = {}; export const ResolveCredentialDefinitionParams = { - credentialDefinitionId: DID_URL.describe( + credentialDefinitionId: CHEQD_DID_URL.describe( 'The DID URL of the credential definition to resolve, e.g., did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009/resources/5acb3d53-ba06-441a-b48b-07d8c2f129f8' ), }; export const RegisterCredentialDefinitionParams = { credentialDefinition: z.object({ - issuerId: DID, - schemaId: DID_URL.describe( + issuerId: CHEQD_DID, + schemaId: CHEQD_DID_URL.describe( 'The DID URL of the schema this credential definition is based on, e.g., did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009/resources/5acb3d53-ba06-441a-b48b-07d8c2f129f8' ), tag: z.string(), @@ -192,21 +198,62 @@ export const ConnectionlessCredentialOfferParams = { .describe( 'Key-value pairs of attributes to be included in the credential, matching the schema defined by credentialDefinitionId' ), - credentialDefinitionId: DID_URL.describe( + credentialDefinitionId: CHEQD_DID_URL.describe( 'The DID URL of the credential definition to use, e.g., did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009/resources/5acb3d53-ba06-441a-b48b-07d8c2f129f8' ), }; -export const CredentialOfferParams = { - connectionId: z.string().uuid(), +export const AnoncredsCredentialOfferParams = z.object({ attributes: z .record(z.string()) .describe( 'Key-value pairs of attributes to be included in the credential, matching the schema defined by credentialDefinitionId' ), - credentialDefinitionId: DID_URL.describe( + credentialDefinitionId: CHEQD_DID_URL.describe( 'The DID URL of the credential definition to use, e.g., did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009/resources/5acb3d53-ba06-441a-b48b-07d8c2f129f8' ), +}); + +const AdditionalDataSchema = z + .object({ + type: z.union([z.string(), z.array(z.string())]), + id: z.string().optional(), + }) + .catchall(z.any()); + +const StatusOptionsSchema = z.object({ + statusPurpose: z.enum(['revocation', 'suspension']), + statusListName: z.string(), + statusListIndex: z.number().optional(), + statusListVersion: z.string().optional(), + statusListRangeStart: z.number().optional(), + statusListRangeEnd: z.number().optional(), + indexNotIn: z.array(z.number()).optional(), +}); + +export const JsonLDCredentialOfferParams = z + .object({ + subjectDid: z.string(), + attributes: z.record(z.unknown()), + '@context': z.array(z.string()).optional(), + type: z.array(z.string()).optional(), + expirationDate: z.union([z.string(), z.date()]).optional(), + issuerDid: z.string(), + credentialStatus: StatusOptionsSchema.optional(), + credentialSchema: z.string().optional(), + credentialName: z.string().optional(), + credentialSummary: z.string().optional(), + termsOfUse: z.union([AdditionalDataSchema, z.array(AdditionalDataSchema)]).optional(), + refreshService: z.union([AdditionalDataSchema, z.array(AdditionalDataSchema)]).optional(), + evidence: z.union([AdditionalDataSchema, z.array(AdditionalDataSchema)]).optional(), + credentialId: z.string().optional(), + }) + .catchall(z.any()); + +export const CredentialOfferParams = { + anoncreds: AnoncredsCredentialOfferParams.optional(), + jsonld: JsonLDCredentialOfferParams.optional(), + connectionId: z.string().uuid(), }; export const ListCredentialParams = {}; @@ -228,13 +275,13 @@ export const ConnectionlessProofRequestParams = { restrictions: z.array( z.object({ cred_def_id: z.optional( - DID_URL.describe( + CHEQD_DID_URL.describe( 'The DID URL of the credential definition to restrict the proof to, e.g., did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009/resources/5acb3d53-ba06-441a-b48b-07d8c2f129f8' ) ), - issuerId: z.optional(DID), + issuerId: z.optional(CHEQD_DID), schemaId: z.optional( - DID_URL.describe( + CHEQD_DID_URL.describe( 'The DID URL of the schema to restrict the proof to, e.g., did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009/resources/5acb3d53-ba06-441a-b48b-07d8c2f129f8' ) ), @@ -253,13 +300,13 @@ export const ConnectionlessProofRequestParams = { restrictions: z.array( z.object({ cred_def_id: z.optional( - DID_URL.describe( + CHEQD_DID_URL.describe( 'The DID URL of the credential definition to restrict the proof to, e.g., did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009/resources/5acb3d53-ba06-441a-b48b-07d8c2f129f8' ) ), - issuerId: z.optional(DID), + issuerId: z.optional(CHEQD_DID), schemaId: z.optional( - DID_URL.describe( + CHEQD_DID_URL.describe( 'The DID URL of the schema to restrict the proof to, e.g., did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009/resources/5acb3d53-ba06-441a-b48b-07d8c2f129f8' ) ), From 491f143d288f2831e3949131a257aa6e3dc06c95 Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Thu, 3 Apr 2025 14:19:57 +0530 Subject: [PATCH 2/4] feat support jsonld in connectionless --- packages/credo/src/tools/credential.ts | 251 +++++++++++++------------ packages/credo/src/types.ts | 17 +- 2 files changed, 135 insertions(+), 133 deletions(-) diff --git a/packages/credo/src/tools/credential.ts b/packages/credo/src/tools/credential.ts index bcd0f21..fa9e9e7 100644 --- a/packages/credo/src/tools/credential.ts +++ b/packages/credo/src/tools/credential.ts @@ -39,49 +39,59 @@ export class CredentialToolHandler { description: 'Creates a connectionless credential offer that can be accepted by any holder. Generates a QR code containing the offer URL, which can be scanned to initiate credential issuance. The response includes the QR code image, outOfBand record, and invitation details.', schema: ConnectionlessCredentialOfferParams, - handler: async ({ attributes, credentialDefinitionId }) => { - let { message, credentialRecord } = await this.credo.agent.credentials.createOffer({ - comment: 'V2 Out of Band offer', - credentialFormats: { - anoncreds: { - attributes: V2CredentialPreview.fromRecord(attributes).attributes, - credentialDefinitionId, - }, - }, - protocolVersion: 'v2', - }); + handler: async ({ jsonld, anoncreds }) => { + try { + const credentialFormats = await this.constructCredentialFormats({ anoncreds, jsonld }); - const { invitationUrl, outOfBandRecord } = - await this.credo.agent.oob.createLegacyConnectionlessInvitation({ - recordId: credentialRecord.id, - message, - domain: this.credo.domain, + const { message, credentialRecord } = await this.credo.agent.credentials.createOffer({ + comment: 'V2 Out of Band offer', + credentialFormats, + protocolVersion: 'v2', + autoAcceptCredential: AutoAcceptCredential.Always, }); - // Generate QR code as a data URL (png format) - const qrCodeBuffer = await QRCode.toBuffer(invitationUrl, { - type: 'png', - margin: 2, - errorCorrectionLevel: 'H', - scale: 8, - }); - return { - content: [ - { - type: 'image', - data: qrCodeBuffer.toString('base64'), - mimeType: 'image/png', - }, - { - type: 'text', - text: JSON.stringify(outOfBandRecord), - }, - { - type: 'text', - text: `Invitation created successfully.\n\nConnection URL: ${invitationUrl}\n\nScan this QR code with another agent to establish a connection:`, - }, - ], - }; + const { invitationUrl, outOfBandRecord } = + await this.credo.agent.oob.createLegacyConnectionlessInvitation({ + recordId: credentialRecord.id, + message, + domain: this.credo.domain, + }); + + // Generate QR code as a data URL (png format) + const qrCodeBuffer = await QRCode.toBuffer(invitationUrl, { + type: 'png', + margin: 2, + errorCorrectionLevel: 'H', + scale: 8, + }); + + return { + content: [ + { + type: 'image', + data: qrCodeBuffer.toString('base64'), + mimeType: 'image/png', + }, + { + type: 'text', + text: JSON.stringify(outOfBandRecord), + }, + { + type: 'text', + text: `Invitation created successfully.\n\nConnection URL: ${invitationUrl}\n\nScan this QR code with another agent to establish a connection:`, + }, + ], + }; + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: error.message, + }, + ], + }; + } }, }; } @@ -97,99 +107,35 @@ export class CredentialToolHandler { 'Creates a credential offer for an existing DIDComm connection. The offer is automatically sent to the connected agent, who can accept it to receive the credential. Returns the credential record with details about the offer status.', schema: CredentialOfferParams, handler: async ({ anoncreds, jsonld, connectionId }) => { - let credentialFormats: CredentialFormatPayload; - if (anoncreds) { - credentialFormats = { - anoncreds: { - attributes: V2CredentialPreview.fromRecord(anoncreds.attributes).attributes, - credentialDefinitionId: anoncreds.credentialDefinitionId, - }, - }; - } else if (jsonld) { - const { - attributes, - credentialName, - credentialSummary, - credentialSchema, - issuerDid, - subjectDid, - type, - expirationDate, - credentialStatus, - context, - format, - connector, - credentialId, - ...additionalData - } = jsonld; + try { + const credentialFormats = await this.constructCredentialFormats({ anoncreds, jsonld }); - // validate issuer DID - const didDocument = await this.credo.agent.dids.resolveDidDocument(issuerDid); - if (!didDocument || !didDocument.assertionMethod) { - return { - content: [ - { - type: 'text', - text: `Issuer DID ${issuerDid} does not exist with an assertionMethod`, - }, - ], - }; - } + let credentialRecord = await this.credo.agent.credentials.offerCredential({ + comment: 'V2 Out of Band offer', + credentialFormats, + protocolVersion: 'v2', + connectionId, + autoAcceptCredential: AutoAcceptCredential.Always, + }); - credentialFormats = { - jsonld: { - credential: { - '@context': [...(context || []), ...VC_CONTEXT], - type: [...(type || []), VC_TYPE], - credentialSubject: { - id: subjectDid, - ...attributes, - }, - issuer: issuerDid, - issuanceDate: new Date().toDateString(), - expirationDate: - expirationDate instanceof Date ? expirationDate.toISOString() : expirationDate, - credentialStatus: credentialStatus - ? { - id: credentialStatus.statusListName, - type: credentialStatus.statusPurpose, - } - : undefined, - ...additionalData, - }, - options: { - proofType: 'Ed25519Signature2018', // validate supported types - proofPurpose: 'assertionMethod', + return { + content: [ + { + type: 'text', + text: JSON.stringify(credentialRecord), }, - } satisfies JsonLdCredentialDetailFormat, + ], }; - } else { + } catch (error: any) { return { content: [ { type: 'text', - text: 'Error credential format jsonld, anoncreds with arguments must be provided', + text: error.message, }, ], }; } - - let credentialRecord = await this.credo.agent.credentials.offerCredential({ - comment: 'V2 Out of Band offer', - credentialFormats: credentialFormats, - protocolVersion: 'v2', - connectionId, - autoAcceptCredential: AutoAcceptCredential.Always, - }); - - return { - content: [ - { - type: 'text', - text: JSON.stringify(credentialRecord), - }, - ], - }; }, }; } @@ -321,4 +267,67 @@ export class CredentialToolHandler { }, }; } + + private async constructCredentialFormats({ anoncreds, jsonld }) { + if (anoncreds) { + return { + anoncreds: { + attributes: V2CredentialPreview.fromRecord(anoncreds.attributes).attributes, + credentialDefinitionId: anoncreds.credentialDefinitionId, + }, + }; + } else if (jsonld) { + const { + attributes, + credentialName, + credentialSummary, + credentialSchema, + issuerDid, + subjectDid, + type, + expirationDate, + credentialStatus, + context, + format, + connector, + credentialId, + ...additionalData + } = jsonld; + + // validate issuer DID + const didDocument = await this.credo.agent.dids.resolveDidDocument(issuerDid); + if (!didDocument || !didDocument.assertionMethod?.length) { + throw new Error(`Issuer DID ${issuerDid} does not exist with an assertionMethod`); + } + + return { + jsonld: { + credential: { + '@context': [...(context || []), ...VC_CONTEXT], + type: [...(type || []), VC_TYPE], + credentialSubject: { + id: subjectDid, + ...attributes, + }, + issuer: issuerDid, + issuanceDate: new Date().toISOString(), + expirationDate: expirationDate instanceof Date ? expirationDate.toISOString() : expirationDate, + credentialStatus: credentialStatus + ? { + id: credentialStatus.statusListName, + type: credentialStatus.statusPurpose, + } + : undefined, + ...additionalData, + }, + options: { + proofType: 'Ed25519Signature2018', + proofPurpose: 'assertionMethod', + }, + } satisfies JsonLdCredentialDetailFormat, + }; + } + + throw new Error('Error credential format jsonld, anoncreds with arguments must be provided'); + } } diff --git a/packages/credo/src/types.ts b/packages/credo/src/types.ts index eb4a655..59c1aa0 100644 --- a/packages/credo/src/types.ts +++ b/packages/credo/src/types.ts @@ -192,17 +192,6 @@ export const GetConnectionRecordParams = { }; // Credential Management Parameters -export const ConnectionlessCredentialOfferParams = { - attributes: z - .record(z.string()) - .describe( - 'Key-value pairs of attributes to be included in the credential, matching the schema defined by credentialDefinitionId' - ), - credentialDefinitionId: CHEQD_DID_URL.describe( - 'The DID URL of the credential definition to use, e.g., did:cheqd:testnet:4769f00d-0af4-472b-aab7-019abbbb8009/resources/5acb3d53-ba06-441a-b48b-07d8c2f129f8' - ), -}; - export const AnoncredsCredentialOfferParams = z.object({ attributes: z .record(z.string()) @@ -250,9 +239,13 @@ export const JsonLDCredentialOfferParams = z }) .catchall(z.any()); -export const CredentialOfferParams = { +export const ConnectionlessCredentialOfferParams = { anoncreds: AnoncredsCredentialOfferParams.optional(), jsonld: JsonLDCredentialOfferParams.optional(), +}; + +export const CredentialOfferParams = { + ...ConnectionlessCredentialOfferParams, connectionId: z.string().uuid(), }; From 48816790408296fc58970e5b446b444de050025c Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Thu, 3 Apr 2025 15:20:49 +0530 Subject: [PATCH 3/4] feat: Support jsonld proofs --- packages/credo/package.json | 1 + packages/credo/src/agent.ts | 5 +- packages/credo/src/tools/proof.ts | 117 ++++++++++++++++-------------- packages/credo/src/types.ts | 41 ++++++++++- pnpm-lock.yaml | 10 +++ 5 files changed, 119 insertions(+), 55 deletions(-) diff --git a/packages/credo/package.json b/packages/credo/package.json index ce69152..314ea2a 100644 --- a/packages/credo/package.json +++ b/packages/credo/package.json @@ -35,6 +35,7 @@ "@hyperledger/aries-askar-nodejs": "^0.2.3", "@modelcontextprotocol/sdk": "^1.8.0", "qrcode": "^1.5.4", + "uuid": "^11.1.0", "zod": "^3.24.2" }, "devDependencies": { diff --git a/packages/credo/src/agent.ts b/packages/credo/src/agent.ts index df7c03f..b789f4a 100644 --- a/packages/credo/src/agent.ts +++ b/packages/credo/src/agent.ts @@ -7,11 +7,13 @@ import { ConnectionsModule, CredentialsModule, DidsModule, + DifPresentationExchangeProofFormatService, HttpOutboundTransport, JsonLdCredentialFormatService, ProofsModule, V2CredentialProtocol, V2ProofProtocol, + W3cCredentialsModule, } from '@credo-ts/core'; import { AskarModule } from '@credo-ts/askar'; import { HttpInboundTransport, agentDependencies } from '@credo-ts/node'; @@ -137,11 +139,12 @@ function getAskarAnonCredsModules(mnemonic: string) { }), ], }), + w3cCredentials: new W3cCredentialsModule(), proofs: new ProofsModule({ autoAcceptProofs: AutoAcceptProof.Always, proofProtocols: [ new V2ProofProtocol({ - proofFormats: [new AnonCredsProofFormatService()], + proofFormats: [new AnonCredsProofFormatService(), new DifPresentationExchangeProofFormatService()], }), ], }), diff --git a/packages/credo/src/tools/proof.ts b/packages/credo/src/tools/proof.ts index d10de26..462aecb 100644 --- a/packages/credo/src/tools/proof.ts +++ b/packages/credo/src/tools/proof.ts @@ -1,5 +1,6 @@ import { AutoAcceptProof } from '@credo-ts/core'; import QRCode from 'qrcode'; +import { v4 } from 'uuid'; import { CredoAgent } from '../agent.js'; import { ConnectionProofRequestParams, @@ -7,7 +8,6 @@ import { GetProofExchangeRecordParams, GetProofRecordParams, ListProofParams, - ProofRequestParams, ToolDefinition, } from '../types.js'; @@ -32,35 +32,12 @@ export class ProofToolHandler { description: 'Creates a connectionless proof request that can be accepted by any holder. Generates a QR code containing the request URL, which can be scanned to initiate proof presentation. The response includes the QR code image and request details.', schema: ConnectionlessProofRequestParams, - handler: async ({ requestedAttributes, requestedPredicates }) => { - const proofAttribute = {}; - requestedAttributes.forEach(({ attribute, restrictions }, index) => { - proofAttribute[`attribute-${index}`] = { - name: attribute, - restrictions, - }; - }); - - const proofPredicate = {}; - requestedPredicates.forEach(({ attribute, restrictions, p_type, p_value }, index) => { - proofPredicate[`predicate-${index}`] = { - name: attribute, - p_type, - p_value, - restrictions, - }; - }); + handler: async ({ jsonld, anoncreds }) => { + const proofFormats = this.constructProofFormats({ jsonld, anoncreds }); let { message, proofRecord } = await this.credo.agent.proofs.createRequest({ comment: 'V2 Out of Band offer', - proofFormats: { - anoncreds: { - name: 'proof-request', - version: '1.0', - requested_attributes: proofAttribute, - requested_predicates: proofPredicate, - }, - }, + proofFormats, protocolVersion: 'v2', }); @@ -109,35 +86,12 @@ export class ProofToolHandler { description: 'Creates a proof request for an existing DIDComm connection. The request is automatically sent to the connected agent, who can respond with the requested proofs. Returns the proof exchange record with details about the request status.', schema: ConnectionProofRequestParams, - handler: async ({ requestedAttributes, requestedPredicates, connectionId }) => { - const proofAttribute = {}; - requestedAttributes.forEach(({ attribute, restrictions }, index) => { - proofAttribute[`attribute-${index}`] = { - name: attribute, - restrictions, - }; - }); - - const proofPredicate = {}; - requestedPredicates.forEach(({ attribute, restrictions, p_type, p_value }, index) => { - proofPredicate[`predicate-${index}`] = { - name: attribute, - p_type, - p_value, - restrictions, - }; - }); + handler: async ({ jsonld, anoncreds, connectionId }) => { + const proofFormats = this.constructProofFormats({ jsonld, anoncreds }); let credentialRecord = await this.credo.agent.proofs.requestProof({ comment: 'V2 Out of Band offer', - proofFormats: { - anoncreds: { - name: 'proof-request', - version: '1.0', - requested_attributes: proofAttribute, - requested_predicates: proofPredicate, - }, - }, + proofFormats, protocolVersion: 'v2', connectionId, autoAcceptProof: AutoAcceptProof.Always, @@ -230,6 +184,11 @@ export class ProofToolHandler { }; } + /** + * Accepts a proof request and generates a Zero-Knowledge Proof from available credentials. + * This tool allows the agent to respond to a proof request by selecting appropriate credentials + * from the wallet and generating a proof that satisfies the request's requirements. + */ acceptProofRequestTool(): ToolDefinition { return { name: 'accept-proof-request', @@ -256,4 +215,56 @@ export class ProofToolHandler { }, }; } + + /** + * Constructs proof formats for either AnonCreds or JSON-LD proof requests. + * Formats the requested attributes and predicates according to the specified proof format type. + * + * @param {Object} params - The parameters for constructing proof formats + * @param {Object} params.anoncreds - AnonCreds proof request parameters + * @param {Object} params.jsonld - JSON-LD proof request parameters + * @returns {Object} Formatted proof request structure + * @throws {Error} If neither anoncreds nor jsonld parameters are provided + */ + private constructProofFormats({ anoncreds, jsonld }) { + if (anoncreds) { + const proofAttribute = {}; + anoncreds.requestedAttributes.forEach(({ attribute, restrictions }, index) => { + proofAttribute[`attribute-${index}`] = { + name: attribute, + restrictions, + }; + }); + + const proofPredicate = {}; + anoncreds.requestedPredicates.forEach(({ attribute, restrictions, p_type, p_value }, index) => { + proofPredicate[`predicate-${index}`] = { + name: attribute, + p_type, + p_value, + restrictions, + }; + }); + + return { + anoncreds: { + name: 'proof-request', + version: '1.0', + requested_attributes: proofAttribute, + requested_predicates: proofPredicate, + }, + }; + } else if (jsonld) { + return { + presentationExchange: { + presentationDefinition: { + id: v4(), + input_descriptors: jsonld || [], + }, + }, + }; + } + + throw new Error('Error proof format jsonld, anoncreds with arguments must be provided'); + } } diff --git a/packages/credo/src/types.ts b/packages/credo/src/types.ts index 59c1aa0..37c46a5 100644 --- a/packages/credo/src/types.ts +++ b/packages/credo/src/types.ts @@ -260,7 +260,41 @@ export const AcceptCredentialOfferParams = { }; // Proof Management Parameters -export const ConnectionlessProofRequestParams = { +const Schema = z.object({ + uri: z.string(), + required: z.boolean().optional(), +}); + +const Issuance = z + .object({ + manifest: z.string().optional(), + // Allow additional properties + }) + .catchall(z.any()); + +const ConstraintsV1 = z.object({ + fields: z + .array( + z.object({ + path: z.array(z.string()), + }) + ) + .optional(), +}); + +const InputDescriptorV1 = z.object({ + id: z.string(), + name: z.string().optional(), + purpose: z.string().optional(), + group: z.array(z.string()).optional(), + schema: z.array(Schema), + issuance: z.array(Issuance).optional(), + constraints: ConstraintsV1.optional(), +}); + +export const PresentationExchangeProofRequestParams = InputDescriptorV1; + +export const AnonCredsProofRequestParams = z.object({ requestedAttributes: z .array( z.object({ @@ -308,6 +342,11 @@ export const ConnectionlessProofRequestParams = { }) ) .describe('List of predicates to be proven without revealing the actual attribute values'), +}); + +export const ConnectionlessProofRequestParams = { + anoncreds: AnonCredsProofRequestParams.optional(), + jsonld: PresentationExchangeProofRequestParams.optional(), }; export const ConnectionProofRequestParams = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1663a8b..7500b11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: qrcode: specifier: ^1.5.4 version: 1.5.4 + uuid: + specifier: ^11.1.0 + version: 11.1.0 zod: specifier: ^3.24.2 version: 3.24.2 @@ -1837,6 +1840,7 @@ packages: '@sphereon/kmp-mdl-mdoc@0.2.0-SNAPSHOT.22': resolution: {integrity: sha512-uAZZExVy+ug9JLircejWa5eLtAZ7bnBP6xb7DO2+86LRsHNLh2k2jMWJYxp+iWtGHTsh6RYsZl14ScQLvjiQ/A==} + bundledDependencies: [] '@sphereon/pex-models@2.3.2': resolution: {integrity: sha512-foFxfLkRwcn/MOp/eht46Q7wsvpQGlO7aowowIIb5Tz9u97kYZ2kz6K2h2ODxWuv5CRA7Q0MY8XUBGE2lfOhOQ==} @@ -6686,6 +6690,10 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + uuid@7.0.3: resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} hasBin: true @@ -15687,6 +15695,8 @@ snapshots: uuid@10.0.0: {} + uuid@11.1.0: {} + uuid@7.0.3: optional: true From 7207dff60fed55b8639b0827b4b97940c31a0534 Mon Sep 17 00:00:00 2001 From: DaevMithran Date: Thu, 3 Apr 2025 16:02:55 +0530 Subject: [PATCH 4/4] fix: Fix VM schema --- packages/credo/src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/credo/src/types.ts b/packages/credo/src/types.ts index 37c46a5..2a90252 100644 --- a/packages/credo/src/types.ts +++ b/packages/credo/src/types.ts @@ -71,6 +71,7 @@ const VerificationMethodSchema = z.object({ controller: z.string(), publicKeyJwk: z.optional(JwkJsonSchema), publicKeyMultibase: z.optional(z.string()), + publicKeyBase58: z.optional(z.string()), }); /**