From cdfee5332afd72443a0c46083800c434055d4ff5 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Wed, 20 May 2026 14:54:55 +0530 Subject: [PATCH 1/9] feat: support W3C JSON-LD credential issuance and presentation Signed-off-by: Sagar Khole --- .../holder/credentialBindingResolver.ts | 2 + .../openid4vc/holder/holder.Controller.ts | 8 + .../openid4vc/holder/holder.service.ts | 100 +++- .../issuance-sessions.service.ts | 146 +++-- .../openid4vc/types/holder.types.ts | 1 + .../openid4vc/types/issuer.types.ts | 32 +- .../openid4vc/types/verifier.types.ts | 3 + .../verification-sessions.service.ts | 17 +- src/enums/enum.ts | 5 + src/routes/routes.ts | 215 +++++--- src/routes/swagger.json | 516 +++++++++++------- src/utils/oid4vc-agent.ts | 87 ++- 12 files changed, 786 insertions(+), 346 deletions(-) diff --git a/src/controllers/openid4vc/holder/credentialBindingResolver.ts b/src/controllers/openid4vc/holder/credentialBindingResolver.ts index 5eb6e12d..6df733dd 100644 --- a/src/controllers/openid4vc/holder/credentialBindingResolver.ts +++ b/src/controllers/openid4vc/holder/credentialBindingResolver.ts @@ -102,6 +102,8 @@ export function getCredentialBindingResolver({ supportsJwk && (credentialFormat === OpenId4VciCredentialFormatProfile.SdJwtVc || credentialFormat === OpenId4VciCredentialFormatProfile.SdJwtDc || + credentialFormat === OpenId4VciCredentialFormatProfile.JwtVcJsonLd || + credentialFormat === OpenId4VciCredentialFormatProfile.JwtVcJson || credentialFormat === OpenId4VciCredentialFormatProfile.MsoMdoc) ) { return { diff --git a/src/controllers/openid4vc/holder/holder.Controller.ts b/src/controllers/openid4vc/holder/holder.Controller.ts index 2e2ad76e..5f4c1e46 100644 --- a/src/controllers/openid4vc/holder/holder.Controller.ts +++ b/src/controllers/openid4vc/holder/holder.Controller.ts @@ -34,6 +34,14 @@ export class HolderController extends Controller { return await holderService.getMdocCredentials(request) } + /** + * Fetch all W3C credentials in wallet + */ + @Get('/w3c-vcs') + public async getW3cCredentials(@Request() request: Req) { + return await holderService.getW3cCredentials(request) + } + /** * Decode mso mdoc credential in wallet */ diff --git a/src/controllers/openid4vc/holder/holder.service.ts b/src/controllers/openid4vc/holder/holder.service.ts index f7970458..027dbcba 100644 --- a/src/controllers/openid4vc/holder/holder.service.ts +++ b/src/controllers/openid4vc/holder/holder.service.ts @@ -14,7 +14,15 @@ import type { } from '@credo-ts/openid4vc' import type { Request as Req } from 'express' -import { Mdoc, SdJwtVcRecord, MdocRecord } from '@credo-ts/core' +import { + Mdoc, + SdJwtVcRecord, + MdocRecord, + W3cCredentialRecord, + W3cCredentialService, + // W3cV2CredentialRecord, + // W3cV2CredentialService, +} from '@credo-ts/core' import { OpenId4VciAuthorizationFlow, authorizationCodeGrantIdentifier, @@ -36,6 +44,22 @@ export class HolderService { return await agentReq.agent.mdoc.getAll() } + public async getW3cCredentials(agentReq: Req) { + const w3cCredentialService = await agentReq.agent.dependencyManager.resolve(W3cCredentialService) + /* + // W3C V2.0 Support + const w3cV2CredentialService = await agentReq.agent.dependencyManager.resolve(W3cV2CredentialService) + + const [v1Records, v2Records] = await Promise.all([ + w3cCredentialService.getAllCredentialRecords(agentReq.agent.context), + w3cV2CredentialService.getAllCredentialRecords(agentReq.agent.context), + ]) + + return [...v1Records, ...v2Records] + */ + return await w3cCredentialService.getAllCredentialRecords(agentReq.agent.context) + } + public async decodeMdocCredential( agentReq: Req, options: { @@ -129,10 +153,28 @@ export class HolderService { const storedCredentials = await Promise.all( credentialResponse.credentials.map(async (response) => { const credentialRecord = response.record - // TODO: We can add this later - // if (credential instanceof W3cJwtVerifiableCredential || credential instanceof W3cJsonLdVerifiableCredential) { - // return await agentReq.agent.w3cCredentials.storeCredential({ credential }) - // } + + if (credentialRecord instanceof W3cCredentialRecord || (credentialRecord as any).type === 'W3cCredentialRecord') { + const w3cCredentialService = await agentReq.agent.dependencyManager.resolve(W3cCredentialService) + return await w3cCredentialService.storeCredential(agentReq.agent.context, { + record: credentialRecord as W3cCredentialRecord, + }) + } + + /* + W3C V2.0 Support + if ( + credentialRecord instanceof W3cV2CredentialRecord || + (credentialRecord as any).type === 'W3cV2CredentialRecord' + ) { + + const w3cCredentialService = await agentReq.agent.dependencyManager.resolve(W3cV2CredentialService) + return await w3cCredentialService.storeCredential(agentReq.agent.context, { + record: credentialRecord as W3cV2CredentialRecord, + }) + } + */ + if (credentialRecord instanceof MdocRecord) { return await agentReq.agent.mdoc.store({ record: credentialRecord }) } @@ -141,7 +183,7 @@ export class HolderService { record: credentialRecord, }) } - throw new Error(`Unsupported credential record type`) + throw new Error(`Unsupported credential record type: ${(credentialRecord as any)?.type || typeof credentialRecord}`) }), ) @@ -211,23 +253,26 @@ export class HolderService { ) // const presentationExchangeService = agent.dependencyManager.resolve(DifPresentationExchangeService) - if (!resolved.dcql) throw new Error('Missing DCQL on request') - // - let dcqlCredentials - try { - dcqlCredentials = await agentReq.agent.modules.openid4vc.holder.selectCredentialsForDcqlRequest( + let acceptOptions: any = { + authorizationRequestPayload: resolved.authorizationRequestPayload, + origin: body.options?.origin, + } + + if (resolved.dcql) { + const dcqlCredentials = await agentReq.agent.modules.openid4vc.holder.selectCredentialsForDcqlRequest( resolved.dcql.queryResult, ) - } catch (error) { - throw error + acceptOptions.dcql = { credentials: dcqlCredentials as DcqlCredentialsForRequest } + } else if (resolved.presentationExchange) { + const pexCredentials = await agentReq.agent.modules.openid4vc.holder.selectCredentialsForPresentationExchangeRequest( + resolved.presentationExchange.credentialsForRequest + ) + acceptOptions.presentationExchange = { credentials: pexCredentials } + } else { + throw new Error('Missing DCQL or Presentation Exchange on request') } - const submissionResult = await agentReq.agent.modules.openid4vc.holder.acceptOpenId4VpAuthorizationRequest({ - authorizationRequestPayload: resolved.authorizationRequestPayload, - dcql: { - credentials: dcqlCredentials as DcqlCredentialsForRequest, - }, - origin: body.options?.origin, - }) + + const submissionResult = await agentReq.agent.modules.openid4vc.holder.acceptOpenId4VpAuthorizationRequest(acceptOptions) if (submissionResult.serverResponse) { const { serverResponse, ...rest } = submissionResult @@ -243,7 +288,20 @@ export class HolderService { } public async deleteCredential(agentReq: Req, { credentialId, credentialType }: DeleteCredentialBody) { - if (credentialType === CredentialType.SD_JWT) { + if (credentialType === CredentialType.W3C_VC) { + const w3cCredentialService = await agentReq.agent.dependencyManager.resolve(W3cCredentialService) + // const w3cV2CredentialService = await agentReq.agent.dependencyManager.resolve(W3cV2CredentialService) + + try { + return await w3cCredentialService.removeCredentialRecord(agentReq.agent.context, credentialId) + } catch (error) { + /* + // W3C V2.0 Support + return await w3cV2CredentialService.removeCredentialRecord(agentReq.agent.context, credentialId) + */ + throw error + } + } else if (credentialType === CredentialType.SD_JWT) { const sdJwtRecord = await agentReq.agent.sdJwtVc.getById(credentialId) if (sdJwtRecord) { return await agentReq.agent.sdJwtVc.deleteById(credentialId) diff --git a/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts b/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts index 97ed3f35..6fc3ecc9 100644 --- a/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts +++ b/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts @@ -4,7 +4,7 @@ import type { Request as Req } from 'express' import { type OpenId4VcIssuanceSessionState } from '@credo-ts/openid4vc' import { OpenId4VcIssuanceSessionRepository } from '@credo-ts/openid4vc' -import { CredentialFormat, SignerMethod } from '../../../enums/enum' +import { CredentialFormat, SignerMethod, W3cContext } from '../../../enums/enum' import { BadRequestError, NotFoundError } from '../../../errors/errors' import { STATUS_LISTS_PATH } from '../../../utils/constant' import { checkAndCreateStatusList, getServerUrl, revokeCredentialInStatusList } from '../../../utils/statusListService' @@ -24,18 +24,27 @@ class IssuanceSessionsService { credentials.map(async (cred) => { const supported = issuer.credentialConfigurationsSupported[cred.credentialSupportedId] - this.validateCredentialConfig(cred, supported) + const format = cred.format as unknown as CredentialFormat + const isJsonLdFormat = format === CredentialFormat.JwtVcJsonLd || format === CredentialFormat.LdpVc + const effectiveVersion = options.version === 'v2.0' && isJsonLdFormat ? 'v2.0' : undefined + + this.validateCredentialConfig(cred, supported, effectiveVersion) const statusBlock = await this.processStatusList(cred, options, agentReq, offerStatusInfo) const currentVct = cred.payload && 'vct' in cred.payload ? cred.payload.vct : undefined - return { - ...cred, - payload: { + const transformedPayload = this.transformPayloadForVersion( + { ...cred.payload, vct: currentVct ?? (typeof supported.vct === 'string' ? supported.vct : undefined), ...(statusBlock ? { status: statusBlock } : {}), }, + effectiveVersion, + ) + + return { + ...cred, + payload: transformedPayload, } }), ) @@ -53,47 +62,20 @@ class IssuanceSessionsService { if (!issuerModule) { throw new Error('OID4VC issuer module not initialized') } - const preAuthorizedCodeFlowConfig = this.resolvePreAuthorizedCodeFlowConfig(options.preAuthorizedCodeFlowConfig) - const { credentialOffer, issuanceSession } = await issuerModule.createCredentialOffer({ issuerId: publicIssuerId, issuanceMetadata: options.issuanceMetadata, credentialConfigurationIds: credentials.map((c) => c.credentialSupportedId), - preAuthorizedCodeFlowConfig, + preAuthorizedCodeFlowConfig: options.preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig: options.authorizationCodeFlowConfig, + // version: options.version ? 'v1.draft15' : 'v1', + version: options.version ? 'v1' : 'v1', }) return { credentialOffer, issuanceSession } } - private resolvePreAuthorizedCodeFlowConfig( - config: OpenId4VcIssuanceSessionsCreateOffer['preAuthorizedCodeFlowConfig'], - ) { - if (!config) return undefined - - const hasTxCode = config.txCode != null - const hasAuthServerUrl = config.authorizationServerUrl != null - - if (hasTxCode !== hasAuthServerUrl) { - throw new BadRequestError( - 'Both txCode and authorizationServerUrl must be provided together for normal flow, or both must be omitted for no-auth flow', - ) - } - - if (!hasTxCode) return {} - - if (Object.keys(config.txCode!).length === 0) { - throw new BadRequestError('txCode must not be an empty object when provided') - } - - if (config.authorizationServerUrl!.trim() === '') { - throw new BadRequestError('authorizationServerUrl must not be an empty string when provided') - } - - return { txCode: config.txCode, authorizationServerUrl: config.authorizationServerUrl } - } - - private validateCredentialConfig(cred: any, supported: any) { + private validateCredentialConfig(cred: any, supported: any, version?: string) { if (!supported) { throw new Error(`CredentialSupportedId '${cred.credentialSupportedId}' is not supported by issuer`) } @@ -103,6 +85,19 @@ class IssuanceSessionsService { ) } + if (!cred.payload?.credentialSubject) { + throw new BadRequestError(`Credential payload for '${cred.credentialSupportedId}' must contain 'credentialSubject'`) + } + + if (version === 'v2.0') { + if (cred.payload.issuer) { + const issuer = cred.payload.issuer + if (typeof issuer === 'object' && !issuer.id) { + throw new BadRequestError(`Issuer object for '${cred.credentialSupportedId}' must contain 'id' property`) + } + } + } + if (!cred.signerOptions?.method) { throw new BadRequestError( `signerOptions must be provided and allowed methods are ${Object.values(SignerMethod).join(', ')}`, @@ -122,6 +117,85 @@ class IssuanceSessionsService { } } + // W3C V2.0 Support (Commented out for reference) + private transformPayloadForVersion(payload: any, version: 'v1.1' | 'v2.0' | undefined) { + /* + if (version !== 'v2.0') { + return payload + } + + const transformed = { ...payload } + + const formatDate = (date: any) => { + if (!date) return undefined + if (date instanceof Date) return date.toISOString() + if (typeof date === 'string') { + try { + const d = new Date(date) + if (isNaN(d.getTime())) return date + return d.toISOString() + } catch { + return date + } + } + return date + } + + // Rule: issuanceDate -> validFrom + if (transformed.issuanceDate && !transformed.validFrom) { + transformed.validFrom = transformed.issuanceDate + + } + + // Rule: expirationDate -> validUntil + if (transformed.expirationDate && !transformed.validUntil) { + transformed.validUntil = transformed.expirationDate + delete transformed.expirationDate + } + + // Normalize dates to ISO format + if (transformed.validFrom) transformed.validFrom = formatDate(transformed.validFrom) + if (transformed.validUntil) transformed.validUntil = formatDate(transformed.validUntil) + + // Rule: issuer string -> object (standardizing for v2.0 if it is a DID) + if (typeof transformed.issuer === 'string' && transformed.issuer.startsWith('did:')) { + transformed.issuer = { id: transformed.issuer } + } + + // Rule: Update @context for v2.0 + const v1Context = W3cContext.V1 + const v2Context = W3cContext.V2 + + if (version === 'v2.0') { + const currentCtx = Array.isArray(transformed['@context']) + ? transformed['@context'] + : typeof transformed['@context'] === 'string' + ? [transformed['@context']] + : [] + + const ctxSet = new Set(currentCtx) + ctxSet.delete(v1Context) + ctxSet.delete(v2Context) + // W3C V2.0 requires the V2 context to be the very first element. + transformed['@context'] = [v2Context, v1Context, ...Array.from(ctxSet)] + } else { + // W3C V1.1 / Default behavior + if (!transformed['@context']) { + transformed['@context'] = [v1Context] + } else if (Array.isArray(transformed['@context'])) { + const ctxSet = new Set(transformed['@context']) + ctxSet.delete(v1Context) + transformed['@context'] = [v1Context, ...Array.from(ctxSet)] + } else if (typeof transformed['@context'] === 'string') { + transformed['@context'] = [v1Context, transformed['@context']] + } + } + + return transformed + */ + return payload + } + private async processStatusList( cred: any, options: OpenId4VcIssuanceSessionsCreateOffer, diff --git a/src/controllers/openid4vc/types/holder.types.ts b/src/controllers/openid4vc/types/holder.types.ts index 852470ca..78745198 100644 --- a/src/controllers/openid4vc/types/holder.types.ts +++ b/src/controllers/openid4vc/types/holder.types.ts @@ -35,4 +35,5 @@ export interface DeleteCredentialBody { export enum CredentialType { SD_JWT = 'sd-jwt-vc', MSO_MDOC = 'mso_mdoc', + W3C_VC = 'w3c-vc', } diff --git a/src/controllers/openid4vc/types/issuer.types.ts b/src/controllers/openid4vc/types/issuer.types.ts index 93b6dca4..8778c83f 100644 --- a/src/controllers/openid4vc/types/issuer.types.ts +++ b/src/controllers/openid4vc/types/issuer.types.ts @@ -1,10 +1,11 @@ -import type { SignerMethod } from '../../../enums/enum' import type { MdocNameSpaces, W3cCredential } from '@credo-ts/core' import type { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' import { Kms } from '@credo-ts/core' import { OpenId4VciCreateCredentialOfferOptions, OpenId4VciSignCredentials } from '@credo-ts/openid4vc' +import { SignerMethod } from '../../../enums/enum' + export interface OpenId4VciOfferCredentials { credentialSupportedId: string format: OpenId4VciCredentialFormatProfile @@ -26,9 +27,24 @@ export interface DisclosureFrameForOffer { [claim: string]: DisclosureFrameForOffer | DisclosureFrameForOffer[] | string[] | undefined } +export interface IssuerObject { + id: string + name?: string + [key: string]: unknown +} + +export interface CredentialSubject { + id?: string + [key: string]: unknown +} + export interface OpenId4VciOfferSdJwtCredential extends OpenId4VciOfferCredentials { payload: { vct?: string + issuer?: string | IssuerObject + credentialSubject?: CredentialSubject | CredentialSubject[] + validFrom?: string | Date + validUntil?: string | Date [key: string]: unknown } disclosureFrame?: DisclosureFrameForOffer @@ -50,8 +66,14 @@ export interface OpenId4VciOfferMdocCredential extends OpenId4VciOfferCredential export interface OpenId4VciOfferW3cCredential extends OpenId4VciOfferCredentials { payload: { - verificationMethod: string - credential: W3cCredential + verificationMethod?: string + credentialSubject?: CredentialSubject | CredentialSubject[] + issuer?: string | { id: string; [key: string]: unknown } + validFrom?: string | Date + validUntil?: string | Date + '@context'?: string | string[] + type?: string | string[] + [key: string]: unknown } } @@ -64,12 +86,13 @@ export interface OpenId4VcIssuanceSessionsCreateOffer { issuerState?: string } preAuthorizedCodeFlowConfig?: { + preAuthorizedCode?: string txCode?: { description?: string length?: number input_mode?: 'numeric' | 'text' } - authorizationServerUrl?: string + authorizationServerUrl: string } issuanceMetadata?: Record statusListDetails?: { @@ -78,6 +101,7 @@ export interface OpenId4VcIssuanceSessionsCreateOffer { listSize?: number } isRevocable?: boolean + version?: 'v1.1' | 'v2.0' } export interface X509GenericRecordContent { diff --git a/src/controllers/openid4vc/types/verifier.types.ts b/src/controllers/openid4vc/types/verifier.types.ts index 9191d0d8..8ce745e9 100644 --- a/src/controllers/openid4vc/types/verifier.types.ts +++ b/src/controllers/openid4vc/types/verifier.types.ts @@ -90,6 +90,7 @@ export interface CreateAuthorizationRequest { requestSigner: OpenId4VcJwtIssuerDid | OpenId4VcIssuerX5cOptions expectedOrigins?: string[] + version?: 'v1' | 'v1.draft21' | 'v1.draft24' } /* -------------------------------------------------------------------------- */ @@ -99,6 +100,8 @@ export interface CreateAuthorizationRequest { export class OpenId4VcSiopVerifierClientMetadata { client_name?: string logo_uri?: string + background_color?: string + text_color?: string } export class OpenId4VcSiopCreateVerifierOptions { diff --git a/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts b/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts index c1aa7850..04824f86 100644 --- a/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts +++ b/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts @@ -63,6 +63,7 @@ export class VerificationSessionsService { const options: any = { requestSigner, verifierId: dto.verifierId, + version: dto.version } if (dto.responseMode === ResponseModeEnum.DC_API || dto.responseMode === ResponseModeEnum.DC_API_JWT) { @@ -71,15 +72,13 @@ export class VerificationSessionsService { if (dto.responseMode) options.responseMode = dto.responseMode if (dto.presentationExchange) { - // options.presentationExchange = dto.presentationExchange - throw new Error('Presentation Exchange is not supported for now') + options.presentationExchange = dto.presentationExchange } if (parsedCertificate) { parsedCertificate.publicJwk.keyId = requestSigner.keyId + options.requestSigner.x5c = [parsedCertificate] } - options.requestSigner.x5c = [parsedCertificate] options.dcql = dto.dcql - // } return (await verifier.createAuthorizationRequest(options)) as any } @@ -178,11 +177,11 @@ export class VerificationSessionsService { ) const dcqlSubmission = verified?.dcql ? Object.entries(verified.dcql.presentations).flatMap(([queryCredentialId, presentations]) => - presentations.map((_, presentationIndex) => ({ - queryCredentialId, - presentationIndex, - })), - ) + presentations.map((_, presentationIndex) => ({ + queryCredentialId, + presentationIndex, + })), + ) : undefined return { diff --git a/src/enums/enum.ts b/src/enums/enum.ts index 8eb51544..23ff5930 100644 --- a/src/enums/enum.ts +++ b/src/enums/enum.ts @@ -119,3 +119,8 @@ export enum CredentialFormat { JwtVcJsonLd = 'jwt_vc_json-ld', LdpVc = 'ldp_vc', } + +export enum W3cContext { + V1 = 'https://www.w3.org/2018/credentials/v1', + V2 = 'https://www.w3.org/ns/credentials/v2', +} diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 11840794..87a6cd86 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -612,6 +612,7 @@ const models: TsoaRoute.Models = { "responseMode": {"ref":"ResponseModeEnum"}, "requestSigner": {"dataType":"union","subSchemas":[{"ref":"OpenId4VcJwtIssuerDid"},{"ref":"OpenId4VcIssuerX5cOptions"}],"required":true}, "expectedOrigins": {"dataType":"array","array":{"dataType":"string"}}, + "version": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["v1"]},{"dataType":"enum","enums":["v1.draft21"]},{"dataType":"enum","enums":["v1.draft24"]}]}, }, "additionalProperties": false, }, @@ -796,6 +797,23 @@ const models: TsoaRoute.Models = { "type": {"ref":"Record_string.unknown_","validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "IssuerObject": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "name": {"dataType":"string"}, + }, + "additionalProperties": {"dataType":"any"}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "CredentialSubject": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string"}, + }, + "additionalProperties": {"dataType":"any"}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "DisclosureFrameForOffer": { "dataType": "refObject", "properties": { @@ -821,7 +839,7 @@ const models: TsoaRoute.Models = { "format": {"ref":"OpenId4VciCredentialFormatProfile","required":true}, "signerOptions": {"dataType":"nestedObjectLiteral","nestedProperties":{"keyId":{"dataType":"string"},"x5c":{"dataType":"array","array":{"dataType":"string"}},"did":{"dataType":"string"},"method":{"ref":"SignerMethod","required":true}},"required":true}, "statusListDetails": {"dataType":"nestedObjectLiteral","nestedProperties":{"listSize":{"dataType":"double"},"index":{"dataType":"double","required":true},"listId":{"dataType":"string","required":true}}}, - "payload": {"dataType":"nestedObjectLiteral","nestedProperties":{"vct":{"dataType":"string"}},"additionalProperties":{"dataType":"any"},"required":true}, + "payload": {"dataType":"nestedObjectLiteral","nestedProperties":{"validUntil":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}]},"validFrom":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}]},"credentialSubject":{"dataType":"union","subSchemas":[{"ref":"CredentialSubject"},{"dataType":"array","array":{"dataType":"refObject","ref":"CredentialSubject"}}]},"issuer":{"dataType":"union","subSchemas":[{"dataType":"string"},{"ref":"IssuerObject"}]},"vct":{"dataType":"string"}},"additionalProperties":{"dataType":"any"},"required":true}, "disclosureFrame": {"ref":"DisclosureFrameForOffer"}, }, "additionalProperties": false, @@ -854,84 +872,6 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "JsonObject": { - "dataType": "refObject", - "properties": { - }, - "additionalProperties": {"ref":"JsonValue"}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "JsonValue": { - "dataType": "refAlias", - "type": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"double"},{"dataType":"boolean"},{"dataType":"enum","enums":[null]},{"ref":"JsonObject"},{"ref":"JsonArray"}],"validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "JsonArray": { - "dataType": "refAlias", - "type": {"dataType":"array","array":{"dataType":"refAlias","ref":"JsonValue"},"validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "W3cIssuer": { - "dataType": "refObject", - "properties": { - "id": {"dataType":"string","required":true}, - }, - "additionalProperties": false, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "W3cCredentialSubject": { - "dataType": "refObject", - "properties": { - "id": {"dataType":"string"}, - "claims": {"ref":"Record_string.unknown_"}, - }, - "additionalProperties": false, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "SingleOrArray_W3cCredentialSubject_": { - "dataType": "refAlias", - "type": {"dataType":"union","subSchemas":[{"ref":"W3cCredentialSubject"},{"dataType":"array","array":{"dataType":"refObject","ref":"W3cCredentialSubject"}}],"validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "W3cCredentialSchema": { - "dataType": "refObject", - "properties": { - "id": {"dataType":"string","required":true}, - "type": {"dataType":"string","required":true}, - }, - "additionalProperties": false, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "SingleOrArray_W3cCredentialSchema_": { - "dataType": "refAlias", - "type": {"dataType":"union","subSchemas":[{"ref":"W3cCredentialSchema"},{"dataType":"array","array":{"dataType":"refObject","ref":"W3cCredentialSchema"}}],"validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "W3cCredentialStatus": { - "dataType": "refObject", - "properties": { - "id": {"dataType":"string","required":true}, - "type": {"dataType":"string","required":true}, - }, - "additionalProperties": false, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "W3cCredential": { - "dataType": "refObject", - "properties": { - "@context": {"dataType":"array","array":{"dataType":"union","subSchemas":[{"dataType":"string"},{"ref":"JsonObject"}]},"required":true}, - "id": {"dataType":"string"}, - "type": {"dataType":"array","array":{"dataType":"string"},"required":true}, - "issuer": {"dataType":"union","subSchemas":[{"dataType":"string"},{"ref":"W3cIssuer"}],"required":true}, - "issuanceDate": {"dataType":"string","required":true}, - "expirationDate": {"dataType":"string"}, - "credentialSubject": {"ref":"SingleOrArray_W3cCredentialSubject_","required":true}, - "credentialSchema": {"ref":"SingleOrArray_W3cCredentialSchema_"}, - "credentialStatus": {"ref":"W3cCredentialStatus"}, - }, - "additionalProperties": false, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "OpenId4VciOfferW3cCredential": { "dataType": "refObject", "properties": { @@ -939,7 +879,7 @@ const models: TsoaRoute.Models = { "format": {"ref":"OpenId4VciCredentialFormatProfile","required":true}, "signerOptions": {"dataType":"nestedObjectLiteral","nestedProperties":{"keyId":{"dataType":"string"},"x5c":{"dataType":"array","array":{"dataType":"string"}},"did":{"dataType":"string"},"method":{"ref":"SignerMethod","required":true}},"required":true}, "statusListDetails": {"dataType":"nestedObjectLiteral","nestedProperties":{"listSize":{"dataType":"double"},"index":{"dataType":"double","required":true},"listId":{"dataType":"string","required":true}}}, - "payload": {"dataType":"nestedObjectLiteral","nestedProperties":{"credential":{"ref":"W3cCredential","required":true},"verificationMethod":{"dataType":"string","required":true}},"required":true}, + "payload": {"dataType":"nestedObjectLiteral","nestedProperties":{"type":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"array","array":{"dataType":"string"}}]},"@context":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"array","array":{"dataType":"string"}}]},"validUntil":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}]},"validFrom":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}]},"issuer":{"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"nestedObjectLiteral","nestedProperties":{"id":{"dataType":"string","required":true}},"additionalProperties":{"dataType":"any"}}]},"credentialSubject":{"dataType":"union","subSchemas":[{"ref":"CredentialSubject"},{"dataType":"array","array":{"dataType":"refObject","ref":"CredentialSubject"}}]},"verificationMethod":{"dataType":"string"}},"additionalProperties":{"dataType":"any"},"required":true}, }, "additionalProperties": false, }, @@ -950,10 +890,11 @@ const models: TsoaRoute.Models = { "publicIssuerId": {"dataType":"string","required":true}, "credentials": {"dataType":"array","array":{"dataType":"union","subSchemas":[{"ref":"OpenId4VciOfferSdJwtCredential"},{"ref":"OpenId4VciOfferMdocCredential"},{"ref":"OpenId4VciOfferW3cCredential"}]},"required":true}, "authorizationCodeFlowConfig": {"dataType":"nestedObjectLiteral","nestedProperties":{"issuerState":{"dataType":"string"},"requirePresentationDuringIssuance":{"dataType":"boolean"},"authorizationServerUrl":{"dataType":"string","required":true}}}, - "preAuthorizedCodeFlowConfig": {"dataType":"nestedObjectLiteral","nestedProperties":{"authorizationServerUrl":{"dataType":"string"},"txCode":{"dataType":"nestedObjectLiteral","nestedProperties":{"input_mode":{"dataType":"union","subSchemas":[{"dataType":"enum","enums":["numeric"]},{"dataType":"enum","enums":["text"]}]},"length":{"dataType":"double"},"description":{"dataType":"string"}}}}}, + "preAuthorizedCodeFlowConfig": {"dataType":"nestedObjectLiteral","nestedProperties":{"authorizationServerUrl":{"dataType":"string","required":true},"txCode":{"dataType":"nestedObjectLiteral","nestedProperties":{"input_mode":{"dataType":"union","subSchemas":[{"dataType":"enum","enums":["numeric"]},{"dataType":"enum","enums":["text"]}]},"length":{"dataType":"double"},"description":{"dataType":"string"}}},"preAuthorizedCode":{"dataType":"string"}}}, "issuanceMetadata": {"ref":"Record_string.unknown_"}, "statusListDetails": {"dataType":"nestedObjectLiteral","nestedProperties":{"listSize":{"dataType":"double"},"index":{"dataType":"double","required":true},"listId":{"dataType":"string","required":true}}}, "isRevocable": {"dataType":"boolean"}, + "version": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["v1.1"]},{"dataType":"enum","enums":["v2.0"]}]}, }, "additionalProperties": false, }, @@ -973,6 +914,11 @@ const models: TsoaRoute.Models = { "type": {"ref":"Record_string.unknown_","validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "W3cCredentialRecord": { + "dataType": "refAlias", + "type": {"ref":"Record_string.unknown_","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ResolveCredentialOfferBody": { "dataType": "refObject", "properties": { @@ -1027,7 +973,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "CredentialType": { "dataType": "refEnum", - "enums": ["sd-jwt-vc","mso_mdoc"], + "enums": ["sd-jwt-vc","mso_mdoc","w3c-vc"], }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "DeleteCredentialBody": { @@ -1146,6 +1092,23 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "JsonValue": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"double"},{"dataType":"boolean"},{"dataType":"enum","enums":[null]},{"ref":"JsonObject"},{"ref":"JsonArray"}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "JsonObject": { + "dataType": "refObject", + "properties": { + }, + "additionalProperties": {"ref":"JsonValue"}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "JsonArray": { + "dataType": "refAlias", + "type": {"dataType":"array","array":{"dataType":"refAlias","ref":"JsonValue"},"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Pick_JwsGeneralFormat.Exclude_keyofJwsGeneralFormat.payload__": { "dataType": "refAlias", "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"header":{"ref":"Record_string.unknown_","required":true},"signature":{"dataType":"string","required":true},"protected":{"dataType":"string","required":true}},"validators":{}}, @@ -1433,11 +1396,6 @@ const models: TsoaRoute.Models = { "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"signature":{"dataType":"string","required":true},"publicKeyBase58":{"dataType":"string","required":true},"keyType":{"dataType":"any","required":true},"data":{"dataType":"string","required":true}},"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "W3cCredentialRecord": { - "dataType": "refAlias", - "type": {"ref":"Record_string.unknown_","validators":{}}, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "W3cJsonIssuer": { "dataType": "refObject", "properties": { @@ -1483,6 +1441,37 @@ const models: TsoaRoute.Models = { "type": {"ref":"Pick_W3cJsonLdSignCredentialOptions.Exclude_keyofW3cJsonLdSignCredentialOptions.format-or-credential__","validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "W3cIssuer": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "W3cCredentialSchema": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "type": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "SingleOrArray_W3cCredentialSchema_": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"ref":"W3cCredentialSchema"},{"dataType":"array","array":{"dataType":"refObject","ref":"W3cCredentialSchema"}}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "W3cCredentialStatus": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "type": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Pick_W3cCredential.Exclude_keyofW3cCredential.context-or-credentialSubject__": { "dataType": "refAlias", "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"issuer":{"dataType":"union","subSchemas":[{"dataType":"string"},{"ref":"W3cIssuer"}],"required":true},"type":{"dataType":"array","array":{"dataType":"string"},"required":true},"id":{"dataType":"string"},"@context":{"dataType":"array","array":{"dataType":"union","subSchemas":[{"dataType":"string"},{"ref":"JsonObject"}]},"required":true},"issuanceDate":{"dataType":"string","required":true},"expirationDate":{"dataType":"string"},"credentialSchema":{"ref":"SingleOrArray_W3cCredentialSchema_"},"credentialStatus":{"ref":"W3cCredentialStatus"}},"validators":{}}, @@ -1600,6 +1589,20 @@ const models: TsoaRoute.Models = { "type": {"dataType":"union","subSchemas":[{"dataType":"union","subSchemas":[{"ref":"LinkedDataProof"},{"ref":"DataIntegrityProof"}]},{"dataType":"array","array":{"dataType":"union","subSchemas":[{"ref":"LinkedDataProof"},{"ref":"DataIntegrityProof"}]}}],"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "W3cCredentialSubject": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string"}, + "claims": {"ref":"Record_string.unknown_"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "SingleOrArray_W3cCredentialSubject_": { + "dataType": "refAlias", + "type": {"dataType":"union","subSchemas":[{"ref":"W3cCredentialSubject"},{"dataType":"array","array":{"dataType":"refObject","ref":"W3cCredentialSubject"}}],"validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "W3cJsonLdVerifiableCredential": { "dataType": "refObject", "properties": { @@ -2346,6 +2349,8 @@ const models: TsoaRoute.Models = { "properties": { "client_name": {"dataType":"string"}, "logo_uri": {"dataType":"string"}, + "background_color": {"dataType":"string"}, + "text_color": {"dataType":"string"}, }, "additionalProperties": false, }, @@ -3428,6 +3433,42 @@ export function RegisterRoutes(app: Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsHolderController_getW3cCredentials: Record = { + request: {"in":"request","name":"request","required":true,"dataType":"object"}, + }; + app.get('/openid4vc/holder/w3c-vcs', + authenticateMiddleware([{"jwt":["tenant","dedicated"]}]), + ...(fetchMiddlewares(HolderController)), + ...(fetchMiddlewares(HolderController.prototype.getW3cCredentials)), + + async function HolderController_getW3cCredentials(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsHolderController_getW3cCredentials, request, response }); + + const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; + + const controller: any = await container.get(HolderController); + if (typeof controller['setStatus'] === 'function') { + controller.setStatus(undefined); + } + + await templateService.apiHandler({ + methodName: 'getW3cCredentials', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const argsHolderController_decodeMdocCredential: Record = { request: {"in":"request","name":"request","required":true,"dataType":"object"}, body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"base64Url":{"dataType":"string","required":true}}}, diff --git a/src/routes/swagger.json b/src/routes/swagger.json index 7f02a7c9..3cd77384 100644 --- a/src/routes/swagger.json +++ b/src/routes/swagger.json @@ -1267,6 +1267,14 @@ "type": "string" }, "type": "array" + }, + "version": { + "type": "string", + "enum": [ + "v1", + "v1.draft21", + "v1.draft24" + ] } }, "required": [ @@ -1647,6 +1655,30 @@ "OpenId4VcIssuanceSessionRecord": { "$ref": "#/components/schemas/Record_string.unknown_" }, + "IssuerObject": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object", + "additionalProperties": {} + }, + "CredentialSubject": { + "properties": { + "id": { + "type": "string" + } + }, + "type": "object", + "additionalProperties": {} + }, "DisclosureFrameForOffer": { "properties": { "_sd": { @@ -1748,6 +1780,51 @@ }, "payload": { "properties": { + "validUntil": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "validFrom": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "credentialSubject": { + "anyOf": [ + { + "$ref": "#/components/schemas/CredentialSubject" + }, + { + "items": { + "$ref": "#/components/schemas/CredentialSubject" + }, + "type": "array" + } + ] + }, + "issuer": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/IssuerObject" + } + ] + }, "vct": { "type": "string" } @@ -1898,182 +1975,6 @@ "type": "object", "additionalProperties": false }, - "JsonObject": { - "properties": {}, - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/JsonValue" - } - }, - "JsonValue": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number", - "format": "double" - }, - { - "type": "boolean" - }, - { - "$ref": "#/components/schemas/JsonObject" - }, - { - "$ref": "#/components/schemas/JsonArray" - } - ], - "nullable": true - }, - "JsonArray": { - "items": { - "$ref": "#/components/schemas/JsonValue" - }, - "type": "array" - }, - "W3cIssuer": { - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ], - "type": "object", - "additionalProperties": false - }, - "W3cCredentialSubject": { - "properties": { - "id": { - "type": "string" - }, - "claims": { - "$ref": "#/components/schemas/Record_string.unknown_" - } - }, - "type": "object", - "additionalProperties": false - }, - "SingleOrArray_W3cCredentialSubject_": { - "anyOf": [ - { - "$ref": "#/components/schemas/W3cCredentialSubject" - }, - { - "items": { - "$ref": "#/components/schemas/W3cCredentialSubject" - }, - "type": "array" - } - ] - }, - "W3cCredentialSchema": { - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "type": "object", - "additionalProperties": false - }, - "SingleOrArray_W3cCredentialSchema_": { - "anyOf": [ - { - "$ref": "#/components/schemas/W3cCredentialSchema" - }, - { - "items": { - "$ref": "#/components/schemas/W3cCredentialSchema" - }, - "type": "array" - } - ] - }, - "W3cCredentialStatus": { - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "id", - "type" - ], - "type": "object", - "additionalProperties": false - }, - "W3cCredential": { - "properties": { - "@context": { - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/components/schemas/JsonObject" - } - ] - }, - "type": "array" - }, - "id": { - "type": "string" - }, - "type": { - "items": { - "type": "string" - }, - "type": "array" - }, - "issuer": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/components/schemas/W3cIssuer" - } - ] - }, - "issuanceDate": { - "type": "string" - }, - "expirationDate": { - "type": "string" - }, - "credentialSubject": { - "$ref": "#/components/schemas/SingleOrArray_W3cCredentialSubject_" - }, - "credentialSchema": { - "$ref": "#/components/schemas/SingleOrArray_W3cCredentialSchema_" - }, - "credentialStatus": { - "$ref": "#/components/schemas/W3cCredentialStatus" - } - }, - "required": [ - "@context", - "type", - "issuer", - "issuanceDate", - "credentialSubject" - ], - "type": "object", - "additionalProperties": false - }, "OpenId4VciOfferW3cCredential": { "properties": { "credentialSupportedId": { @@ -2127,17 +2028,91 @@ }, "payload": { "properties": { - "credential": { - "$ref": "#/components/schemas/W3cCredential" + "type": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "@context": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + }, + "validUntil": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "validFrom": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "issuer": { + "anyOf": [ + { + "type": "string" + }, + { + "properties": { + "id": { + "type": "string" + } + }, + "additionalProperties": {}, + "required": [ + "id" + ], + "type": "object" + } + ] + }, + "credentialSubject": { + "anyOf": [ + { + "$ref": "#/components/schemas/CredentialSubject" + }, + { + "items": { + "$ref": "#/components/schemas/CredentialSubject" + }, + "type": "array" + } + ] }, "verificationMethod": { "type": "string" } }, - "required": [ - "credential", - "verificationMethod" - ], + "additionalProperties": {}, "type": "object" } }, @@ -2211,8 +2186,14 @@ } }, "type": "object" + }, + "preAuthorizedCode": { + "type": "string" } }, + "required": [ + "authorizationServerUrl" + ], "type": "object" }, "issuanceMetadata": { @@ -2240,6 +2221,13 @@ }, "isRevocable": { "type": "boolean" + }, + "version": { + "type": "string", + "enum": [ + "v1.1", + "v2.0" + ] } }, "required": [ @@ -2270,6 +2258,9 @@ "MdocRecord": { "$ref": "#/components/schemas/Record_string.unknown_" }, + "W3cCredentialRecord": { + "$ref": "#/components/schemas/Record_string.unknown_" + }, "ResolveCredentialOfferBody": { "properties": { "credentialOfferUri": { @@ -2366,7 +2357,8 @@ "CredentialType": { "enum": [ "sd-jwt-vc", - "mso_mdoc" + "mso_mdoc", + "w3c-vc" ], "type": "string" }, @@ -2568,6 +2560,40 @@ "type": "object", "additionalProperties": false }, + "JsonValue": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number", + "format": "double" + }, + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/JsonObject" + }, + { + "$ref": "#/components/schemas/JsonArray" + } + ], + "nullable": true + }, + "JsonObject": { + "properties": {}, + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + } + }, + "JsonArray": { + "items": { + "$ref": "#/components/schemas/JsonValue" + }, + "type": "array" + }, "Pick_JwsGeneralFormat.Exclude_keyofJwsGeneralFormat.payload__": { "properties": { "header": { @@ -3263,9 +3289,6 @@ ], "type": "object" }, - "W3cCredentialRecord": { - "$ref": "#/components/schemas/Record_string.unknown_" - }, "W3cJsonIssuer": { "properties": { "id": { @@ -3380,6 +3403,63 @@ "$ref": "#/components/schemas/Pick_W3cJsonLdSignCredentialOptions.Exclude_keyofW3cJsonLdSignCredentialOptions.format-or-credential__", "description": "Construct a type with the properties of T except for those in type K." }, + "W3cIssuer": { + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object", + "additionalProperties": false + }, + "W3cCredentialSchema": { + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "type" + ], + "type": "object", + "additionalProperties": false + }, + "SingleOrArray_W3cCredentialSchema_": { + "anyOf": [ + { + "$ref": "#/components/schemas/W3cCredentialSchema" + }, + { + "items": { + "$ref": "#/components/schemas/W3cCredentialSchema" + }, + "type": "array" + } + ] + }, + "W3cCredentialStatus": { + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "type" + ], + "type": "object", + "additionalProperties": false + }, "Pick_W3cCredential.Exclude_keyofW3cCredential.context-or-credentialSubject__": { "properties": { "issuer": { @@ -3754,6 +3834,31 @@ } ] }, + "W3cCredentialSubject": { + "properties": { + "id": { + "type": "string" + }, + "claims": { + "$ref": "#/components/schemas/Record_string.unknown_" + } + }, + "type": "object", + "additionalProperties": false + }, + "SingleOrArray_W3cCredentialSubject_": { + "anyOf": [ + { + "$ref": "#/components/schemas/W3cCredentialSubject" + }, + { + "items": { + "$ref": "#/components/schemas/W3cCredentialSubject" + }, + "type": "array" + } + ] + }, "W3cJsonLdVerifiableCredential": { "properties": { "@context": { @@ -5482,6 +5587,12 @@ }, "logo_uri": { "type": "string" + }, + "background_color": { + "type": "string" + }, + "text_color": { + "type": "string" } }, "type": "object", @@ -7081,6 +7192,39 @@ "parameters": [] } }, + "/openid4vc/holder/w3c-vcs": { + "get": { + "operationId": "GetW3cCredentials", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/W3cCredentialRecord" + }, + "type": "array" + } + } + } + } + }, + "description": "Fetch all W3C credentials in wallet", + "tags": [ + "oid4vc holders" + ], + "security": [ + { + "jwt": [ + "tenant", + "dedicated" + ] + } + ], + "parameters": [] + } + }, "/openid4vc/holder/mdoc-vcs/decode": { "post": { "operationId": "DecodeMdocCredential", diff --git a/src/utils/oid4vc-agent.ts b/src/utils/oid4vc-agent.ts index e8de5aee..85882f15 100644 --- a/src/utils/oid4vc-agent.ts +++ b/src/utils/oid4vc-agent.ts @@ -17,11 +17,15 @@ import { X509Certificate, X509ModuleConfig, X509Service, + W3cCredential, + W3cV2Credential, + JsonTransformer, } from '@credo-ts/core' +import * as fs from 'fs' import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' import { container } from 'tsyringe' -import { SignerMethod } from '../enums/enum' +import { SignerMethod, W3cContext } from '../enums/enum' import { validateAuthConfig } from './auth' import { checkX509Certificates, processIsoImages } from './helpers' @@ -38,7 +42,8 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent agentContext, authorization, }) => { - const issuanceMetadata = issuanceSession.issuanceMetadata + try { + const issuanceMetadata = issuanceSession.issuanceMetadata if (!issuanceMetadata?.['credentials']) throw new Error('credential payload is not provided') const allCredentialPayload = issuanceMetadata?.['credentials'] @@ -185,7 +190,77 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent } satisfies OpenId4VciSignSdJwtCredentials } - throw new Error('Invalid request') + if ( + credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJson || + credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd + ) { + const payload = credentialPayload[0]?.payload; + const context = payload?.['@context']; + const contextArray = Array.isArray(context) ? context : context ? [context] : []; + const isV2 = contextArray.includes(W3cContext.V2) || !!payload?.validFrom; + + return { + format: ClaimFormat.JwtVc, + credentials: holderBinding.keys.map((binding) => { + // If the binding is JWK and not DID, we should probably handle it or skip throwing + const rawSubject = { ...(payload.credentialSubject || {}) }; + const { id: subjectIdRaw, ...claims } = rawSubject; + const subjectId = subjectIdRaw || (binding.method === 'did' ? (binding as any).didUrl : undefined); + + if (subjectId) { + rawSubject.id = subjectId; + } + + const issuer = payload.issuer || credential.signerOptions.did; + const finalIssuer = (isV2 && typeof issuer === 'string') ? { id: issuer } : issuer; + + const mainContext = isV2 ? W3cContext.V2 : W3cContext.V1; + + const finalContext = contextArray.includes(mainContext) + ? [mainContext, ...contextArray.filter(c => c !== mainContext)] + : [mainContext, ...contextArray]; + + const credentialJson: any = { + '@context': finalContext, + type: payload.type, + issuer: finalIssuer, + credentialSubject: rawSubject, + }; + + if (isV2) { + credentialJson.validFrom = payload.validFrom || payload.issuanceDate; + credentialJson.validUntil = payload.validUntil || payload.expirationDate; + // Add issuanceDate for JWT signer compatibility + credentialJson.issuanceDate = credentialJson.validFrom; + credentialJson.expirationDate = credentialJson.validUntil; + } else { + credentialJson.issuanceDate = payload.issuanceDate; + credentialJson.expirationDate = payload.expirationDate; + } + + const credInstance: any = isV2 + ? JsonTransformer.fromJSON(credentialJson, W3cV2Credential) + : JsonTransformer.fromJSON(credentialJson, W3cCredential); + + if (credInstance.credentialSubject) { + credInstance.credentialSubject.id = subjectId; + } + + return { + format: ClaimFormat.JwtVc, + verificationMethod: issuerDidVerificationMethod!, + credential: credInstance + }; + }), + type: 'credentials', + } as any + } + + throw new Error('Invalid request format ' + credentialConfiguration.format) + } catch (e: any) { + fs.appendFileSync('mapper-error.log', e.stack + '\n'); + throw e; + } } } @@ -232,6 +307,12 @@ export interface OpenId4VcIssuanceSessionCreateOfferSdJwtCredentialOptions { */ payload: { vct?: string + issuer?: string | any + credentialSubject?: any + validFrom?: string + validUntil?: string + issuanceDate?: string + expirationDate?: string [key: string]: unknown } From 2a653b17959f6298dd43013693e41c27086dfba6 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Fri, 22 May 2026 14:49:29 +0530 Subject: [PATCH 2/9] fix:Code review comments Signed-off-by: Sagar Khole --- .../openid4vc/holder/holder.service.ts | 35 ++++++++++--------- .../issuance-sessions.service.ts | 27 +++++++------- .../openid4vc/types/issuer.types.ts | 6 +--- .../verification-sessions.service.ts | 23 ++++++++---- src/enums/enum.ts | 5 --- 5 files changed, 50 insertions(+), 46 deletions(-) diff --git a/src/controllers/openid4vc/holder/holder.service.ts b/src/controllers/openid4vc/holder/holder.service.ts index 027dbcba..8ad6e0b6 100644 --- a/src/controllers/openid4vc/holder/holder.service.ts +++ b/src/controllers/openid4vc/holder/holder.service.ts @@ -45,19 +45,16 @@ export class HolderService { } public async getW3cCredentials(agentReq: Req) { - const w3cCredentialService = await agentReq.agent.dependencyManager.resolve(W3cCredentialService) /* // W3C V2.0 Support - const w3cV2CredentialService = await agentReq.agent.dependencyManager.resolve(W3cV2CredentialService) - const [v1Records, v2Records] = await Promise.all([ - w3cCredentialService.getAllCredentialRecords(agentReq.agent.context), - w3cV2CredentialService.getAllCredentialRecords(agentReq.agent.context), + agentReq.agent.w3cCredentials.getAll(), + agentReq.agent.w3cV2Credentials.getAll(), ]) return [...v1Records, ...v2Records] */ - return await w3cCredentialService.getAllCredentialRecords(agentReq.agent.context) + return await agentReq.agent.w3cCredentials.getAll() } public async decodeMdocCredential( @@ -154,9 +151,11 @@ export class HolderService { credentialResponse.credentials.map(async (response) => { const credentialRecord = response.record - if (credentialRecord instanceof W3cCredentialRecord || (credentialRecord as any).type === 'W3cCredentialRecord') { - const w3cCredentialService = await agentReq.agent.dependencyManager.resolve(W3cCredentialService) - return await w3cCredentialService.storeCredential(agentReq.agent.context, { + if ( + credentialRecord instanceof W3cCredentialRecord || + (credentialRecord as any).type === 'W3cCredentialRecord' + ) { + return await agentReq.agent.w3cCredentials.store({ record: credentialRecord as W3cCredentialRecord, }) } @@ -167,9 +166,7 @@ export class HolderService { credentialRecord instanceof W3cV2CredentialRecord || (credentialRecord as any).type === 'W3cV2CredentialRecord' ) { - - const w3cCredentialService = await agentReq.agent.dependencyManager.resolve(W3cV2CredentialService) - return await w3cCredentialService.storeCredential(agentReq.agent.context, { + return await agentReq.agent.w3cV2Credentials.store({ record: credentialRecord as W3cV2CredentialRecord, }) } @@ -183,7 +180,9 @@ export class HolderService { record: credentialRecord, }) } - throw new Error(`Unsupported credential record type: ${(credentialRecord as any)?.type || typeof credentialRecord}`) + throw new Error( + `Unsupported credential record type: ${(credentialRecord as any)?.type || typeof credentialRecord}`, + ) }), ) @@ -264,15 +263,17 @@ export class HolderService { ) acceptOptions.dcql = { credentials: dcqlCredentials as DcqlCredentialsForRequest } } else if (resolved.presentationExchange) { - const pexCredentials = await agentReq.agent.modules.openid4vc.holder.selectCredentialsForPresentationExchangeRequest( - resolved.presentationExchange.credentialsForRequest - ) + const pexCredentials = + await agentReq.agent.modules.openid4vc.holder.selectCredentialsForPresentationExchangeRequest( + resolved.presentationExchange.credentialsForRequest, + ) acceptOptions.presentationExchange = { credentials: pexCredentials } } else { throw new Error('Missing DCQL or Presentation Exchange on request') } - const submissionResult = await agentReq.agent.modules.openid4vc.holder.acceptOpenId4VpAuthorizationRequest(acceptOptions) + const submissionResult = + await agentReq.agent.modules.openid4vc.holder.acceptOpenId4VpAuthorizationRequest(acceptOptions) if (submissionResult.serverResponse) { const { serverResponse, ...rest } = submissionResult diff --git a/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts b/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts index 6fc3ecc9..d0b32717 100644 --- a/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts +++ b/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts @@ -4,7 +4,9 @@ import type { Request as Req } from 'express' import { type OpenId4VcIssuanceSessionState } from '@credo-ts/openid4vc' import { OpenId4VcIssuanceSessionRepository } from '@credo-ts/openid4vc' -import { CredentialFormat, SignerMethod, W3cContext } from '../../../enums/enum' +import { CREDENTIALS_CONTEXT_V1_URL, CREDENTIALS_CONTEXT_V2_URL } from '@credo-ts/core' + +import { CredentialFormat, SignerMethod } from '../../../enums/enum' import { BadRequestError, NotFoundError } from '../../../errors/errors' import { STATUS_LISTS_PATH } from '../../../utils/constant' import { checkAndCreateStatusList, getServerUrl, revokeCredentialInStatusList } from '../../../utils/statusListService' @@ -68,8 +70,7 @@ class IssuanceSessionsService { credentialConfigurationIds: credentials.map((c) => c.credentialSupportedId), preAuthorizedCodeFlowConfig: options.preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig: options.authorizationCodeFlowConfig, - // version: options.version ? 'v1.draft15' : 'v1', - version: options.version ? 'v1' : 'v1', + version: 'v1', }) return { credentialOffer, issuanceSession } @@ -85,8 +86,15 @@ class IssuanceSessionsService { ) } - if (!cred.payload?.credentialSubject) { - throw new BadRequestError(`Credential payload for '${cred.credentialSupportedId}' must contain 'credentialSubject'`) + const isW3cFormat = + cred.format === CredentialFormat.JwtVcJson || + cred.format === CredentialFormat.JwtVcJsonLd || + cred.format === CredentialFormat.LdpVc + + if (isW3cFormat && !cred.payload?.credentialSubject) { + throw new BadRequestError( + `Credential payload for '${cred.credentialSupportedId}' must contain 'credentialSubject'`, + ) } if (version === 'v2.0') { @@ -117,9 +125,7 @@ class IssuanceSessionsService { } } - // W3C V2.0 Support (Commented out for reference) private transformPayloadForVersion(payload: any, version: 'v1.1' | 'v2.0' | undefined) { - /* if (version !== 'v2.0') { return payload } @@ -144,7 +150,6 @@ class IssuanceSessionsService { // Rule: issuanceDate -> validFrom if (transformed.issuanceDate && !transformed.validFrom) { transformed.validFrom = transformed.issuanceDate - } // Rule: expirationDate -> validUntil @@ -163,8 +168,8 @@ class IssuanceSessionsService { } // Rule: Update @context for v2.0 - const v1Context = W3cContext.V1 - const v2Context = W3cContext.V2 + const v1Context = CREDENTIALS_CONTEXT_V1_URL + const v2Context = CREDENTIALS_CONTEXT_V2_URL if (version === 'v2.0') { const currentCtx = Array.isArray(transformed['@context']) @@ -192,8 +197,6 @@ class IssuanceSessionsService { } return transformed - */ - return payload } private async processStatusList( diff --git a/src/controllers/openid4vc/types/issuer.types.ts b/src/controllers/openid4vc/types/issuer.types.ts index 8778c83f..0ff32bf9 100644 --- a/src/controllers/openid4vc/types/issuer.types.ts +++ b/src/controllers/openid4vc/types/issuer.types.ts @@ -1,11 +1,7 @@ +import type { SignerMethod } from '../../../enums/enum' import type { MdocNameSpaces, W3cCredential } from '@credo-ts/core' import type { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' -import { Kms } from '@credo-ts/core' -import { OpenId4VciCreateCredentialOfferOptions, OpenId4VciSignCredentials } from '@credo-ts/openid4vc' - -import { SignerMethod } from '../../../enums/enum' - export interface OpenId4VciOfferCredentials { credentialSupportedId: string format: OpenId4VciCredentialFormatProfile diff --git a/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts b/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts index 04824f86..5b0a8214 100644 --- a/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts +++ b/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts @@ -63,22 +63,31 @@ export class VerificationSessionsService { const options: any = { requestSigner, verifierId: dto.verifierId, - version: dto.version + version: dto.version, } if (dto.responseMode === ResponseModeEnum.DC_API || dto.responseMode === ResponseModeEnum.DC_API_JWT) { options.expectedOrigins = dto.expectedOrigins } + if (dto.presentationExchange && dto.dcql) { + throw new Error('Only one of presentationExchange or dcql can be provided, not both') + } + if (!dto.presentationExchange && !dto.dcql) { + throw new Error('Either presentationExchange or dcql must be provided') + } + if (dto.responseMode) options.responseMode = dto.responseMode if (dto.presentationExchange) { options.presentationExchange = dto.presentationExchange + } else { + options.dcql = dto.dcql } + if (parsedCertificate) { parsedCertificate.publicJwk.keyId = requestSigner.keyId options.requestSigner.x5c = [parsedCertificate] } - options.dcql = dto.dcql return (await verifier.createAuthorizationRequest(options)) as any } @@ -177,11 +186,11 @@ export class VerificationSessionsService { ) const dcqlSubmission = verified?.dcql ? Object.entries(verified.dcql.presentations).flatMap(([queryCredentialId, presentations]) => - presentations.map((_, presentationIndex) => ({ - queryCredentialId, - presentationIndex, - })), - ) + presentations.map((_, presentationIndex) => ({ + queryCredentialId, + presentationIndex, + })), + ) : undefined return { diff --git a/src/enums/enum.ts b/src/enums/enum.ts index 23ff5930..8eb51544 100644 --- a/src/enums/enum.ts +++ b/src/enums/enum.ts @@ -119,8 +119,3 @@ export enum CredentialFormat { JwtVcJsonLd = 'jwt_vc_json-ld', LdpVc = 'ldp_vc', } - -export enum W3cContext { - V1 = 'https://www.w3.org/2018/credentials/v1', - V2 = 'https://www.w3.org/ns/credentials/v2', -} From 5cd2a6da4b1baaee052c50c18cff69c2e4557a9d Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Mon, 25 May 2026 09:04:03 +0530 Subject: [PATCH 3/9] feat: support x509 certificate chains and fix mdoc date encoding - Updated mDoc issuance to parse and include full X.509 certificate chains. - Fixed CBOR date encoding in mDoc validityInfo by casting to Date objects. - Added explicit error for unsupported x5c signing in W3C VC formats. - Refactored W3C VC mapping logic for better context detection (V1 vs V2). Signed-off-by: Sagar Khole --- src/cliAgent.ts | 4 +- src/utils/oid4vc-agent.ts | 175 ++++++++++++++++++++++---------------- 2 files changed, 103 insertions(+), 76 deletions(-) diff --git a/src/cliAgent.ts b/src/cliAgent.ts index c2575ddd..fa320122 100644 --- a/src/cliAgent.ts +++ b/src/cliAgent.ts @@ -211,8 +211,8 @@ const getModules = ( }), w3cCredentials: isCustomDocumentLoaderEnabled() ? new W3cCredentialsModule({ - documentLoader: CustomDocumentLoader, - }) + documentLoader: CustomDocumentLoader, + }) : new W3cCredentialsModule(), didcomm: new DidCommModule({ processDidCommMessagesConcurrently: true, diff --git a/src/utils/oid4vc-agent.ts b/src/utils/oid4vc-agent.ts index 85882f15..669435a1 100644 --- a/src/utils/oid4vc-agent.ts +++ b/src/utils/oid4vc-agent.ts @@ -20,12 +20,13 @@ import { W3cCredential, W3cV2Credential, JsonTransformer, + CREDENTIALS_CONTEXT_V1_URL, + CREDENTIALS_CONTEXT_V2_URL, } from '@credo-ts/core' -import * as fs from 'fs' import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' import { container } from 'tsyringe' -import { SignerMethod, W3cContext } from '../enums/enum' +import { SignerMethod } from '../enums/enum' import { validateAuthConfig } from './auth' import { checkX509Certificates, processIsoImages } from './helpers' @@ -42,8 +43,7 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent agentContext, authorization, }) => { - try { - const issuanceMetadata = issuanceSession.issuanceMetadata + const issuanceMetadata = issuanceSession.issuanceMetadata if (!issuanceMetadata?.['credentials']) throw new Error('credential payload is not provided') const allCredentialPayload = issuanceMetadata?.['credentials'] @@ -123,22 +123,26 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent throw new Error(`'doctype' not found in credential configuration,`) } - const parsedCertificate = X509Service.parseCertificate(agentContext, { - encodedCertificate: issuerx509certificate[0], + const parsedCertificates = issuerx509certificate.map((cert) => { + return X509Service.parseCertificate(agentContext, { + encodedCertificate: cert, + }) }) - parsedCertificate.publicJwk.keyId = credential.signerOptions.keyId + parsedCertificates[0].publicJwk.keyId = credential.signerOptions.keyId const updatedNamespaces = processIsoImages(credential.payload.namespaces) credential.payload.namespaces = updatedNamespaces return { type: 'credentials', format: ClaimFormat.MsoMdoc, credentials: holderBinding.keys.map((holderBindingDetails) => ({ - issuerCertificate: parsedCertificate, + issuerCertificate: parsedCertificates as any, holderKey: holderBindingDetails.jwk, ...credential.payload, validityInfo: { - ...credential.payload.validityInfo, + signed: credential.payload.validityInfo.signed + ? new Date(credential.payload.validityInfo.signed) + : new Date(), validFrom: new Date(credential.payload.validityInfo.validFrom), validUntil: new Date(credential.payload.validityInfo.validUntil), }, @@ -190,77 +194,100 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent } satisfies OpenId4VciSignSdJwtCredentials } - if ( - credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJson || - credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd - ) { - const payload = credentialPayload[0]?.payload; - const context = payload?.['@context']; - const contextArray = Array.isArray(context) ? context : context ? [context] : []; - const isV2 = contextArray.includes(W3cContext.V2) || !!payload?.validFrom; - - return { - format: ClaimFormat.JwtVc, - credentials: holderBinding.keys.map((binding) => { - // If the binding is JWK and not DID, we should probably handle it or skip throwing - const rawSubject = { ...(payload.credentialSubject || {}) }; - const { id: subjectIdRaw, ...claims } = rawSubject; - const subjectId = subjectIdRaw || (binding.method === 'did' ? (binding as any).didUrl : undefined); + if ( + credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJson || + credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd + ) { + if (credential.signerOptions.method === SignerMethod.X5c) { + throw new Error(`X5c signing method is not supported for W3C VC formats (${credentialConfiguration.format})`) + } + + const payload = credentialPayload[0]?.payload + const context = payload?.['@context'] + const contextArray = Array.isArray(context) ? context : context ? [context] : [] + const isV2 = contextArray.includes(CREDENTIALS_CONTEXT_V2_URL) || !!payload?.validFrom + return { + format: ClaimFormat.JwtVc, + credentials: holderBinding.keys.map((binding) => { + let rawSubject: any + let subjectId: string | undefined = undefined + const bindingDid = binding.method === 'did' ? (binding as any).didUrl : undefined + + if (Array.isArray(payload.credentialSubject)) { + rawSubject = payload.credentialSubject.map((subj: any) => { + if (subj && typeof subj === 'object') { + const itemId = subj.id || bindingDid + return itemId ? { ...subj, id: itemId } : { ...subj } + } + return subj + }) + const firstWithId = rawSubject.find((s: any) => s?.id) + subjectId = firstWithId?.id || bindingDid + } else { + const origSubject = payload.credentialSubject || {} + subjectId = origSubject.id || bindingDid + + rawSubject = { ...origSubject } if (subjectId) { - rawSubject.id = subjectId; + rawSubject.id = subjectId } - - const issuer = payload.issuer || credential.signerOptions.did; - const finalIssuer = (isV2 && typeof issuer === 'string') ? { id: issuer } : issuer; - - const mainContext = isV2 ? W3cContext.V2 : W3cContext.V1; - - const finalContext = contextArray.includes(mainContext) - ? [mainContext, ...contextArray.filter(c => c !== mainContext)] - : [mainContext, ...contextArray]; - - const credentialJson: any = { - '@context': finalContext, - type: payload.type, - issuer: finalIssuer, - credentialSubject: rawSubject, - }; - - if (isV2) { - credentialJson.validFrom = payload.validFrom || payload.issuanceDate; - credentialJson.validUntil = payload.validUntil || payload.expirationDate; - // Add issuanceDate for JWT signer compatibility - credentialJson.issuanceDate = credentialJson.validFrom; - credentialJson.expirationDate = credentialJson.validUntil; + } + + const issuer = payload.issuer || credential.signerOptions.did + const finalIssuer = isV2 && typeof issuer === 'string' ? { id: issuer } : issuer + + const mainContext = isV2 ? CREDENTIALS_CONTEXT_V2_URL : CREDENTIALS_CONTEXT_V1_URL + + const finalContext = contextArray.includes(mainContext) + ? [mainContext, ...contextArray.filter((c) => c !== mainContext)] + : [mainContext, ...contextArray] + + const credentialJson: any = { + '@context': finalContext, + type: payload.type, + issuer: finalIssuer, + credentialSubject: rawSubject, + } + + if (isV2) { + credentialJson.validFrom = payload.validFrom || payload.issuanceDate + credentialJson.validUntil = payload.validUntil || payload.expirationDate + // Add issuanceDate for JWT signer compatibility + credentialJson.issuanceDate = credentialJson.validFrom + credentialJson.expirationDate = credentialJson.validUntil + } else { + credentialJson.issuanceDate = payload.issuanceDate + credentialJson.expirationDate = payload.expirationDate + } + + const credInstance: any = isV2 + ? JsonTransformer.fromJSON(credentialJson, W3cV2Credential) + : JsonTransformer.fromJSON(credentialJson, W3cCredential) + + if (credInstance.credentialSubject) { + if (Array.isArray(credInstance.credentialSubject)) { + for (const item of credInstance.credentialSubject) { + if (item && typeof item === 'object') { + item.id = item.id || subjectId + } + } } else { - credentialJson.issuanceDate = payload.issuanceDate; - credentialJson.expirationDate = payload.expirationDate; + credInstance.credentialSubject.id = subjectId } - - const credInstance: any = isV2 - ? JsonTransformer.fromJSON(credentialJson, W3cV2Credential) - : JsonTransformer.fromJSON(credentialJson, W3cCredential); - - if (credInstance.credentialSubject) { - credInstance.credentialSubject.id = subjectId; - } - - return { - format: ClaimFormat.JwtVc, - verificationMethod: issuerDidVerificationMethod!, - credential: credInstance - }; - }), - type: 'credentials', - } as any - } - - throw new Error('Invalid request format ' + credentialConfiguration.format) - } catch (e: any) { - fs.appendFileSync('mapper-error.log', e.stack + '\n'); - throw e; + } + + return { + format: ClaimFormat.JwtVc, + verificationMethod: issuerDidVerificationMethod, + credential: credInstance, + } + }), + type: 'credentials', + } as any } + + throw new Error('Invalid request format ' + credentialConfiguration.format) } } From 3d8403dcc66303ae4c5331a325fcd56d1e3029ab Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Mon, 25 May 2026 09:31:32 +0530 Subject: [PATCH 4/9] Enhanced W3C VC 2.0 detection and prevented empty x5c crashes. Signed-off-by: Sagar Khole --- src/cliAgent.ts | 4 ++-- src/utils/oid4vc-agent.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cliAgent.ts b/src/cliAgent.ts index fa320122..c2575ddd 100644 --- a/src/cliAgent.ts +++ b/src/cliAgent.ts @@ -211,8 +211,8 @@ const getModules = ( }), w3cCredentials: isCustomDocumentLoaderEnabled() ? new W3cCredentialsModule({ - documentLoader: CustomDocumentLoader, - }) + documentLoader: CustomDocumentLoader, + }) : new W3cCredentialsModule(), didcomm: new DidCommModule({ processDidCommMessagesConcurrently: true, diff --git a/src/utils/oid4vc-agent.ts b/src/utils/oid4vc-agent.ts index 669435a1..208f06f7 100644 --- a/src/utils/oid4vc-agent.ts +++ b/src/utils/oid4vc-agent.ts @@ -114,9 +114,9 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent } if (credentialConfiguration.format === OpenId4VciCredentialFormatProfile.MsoMdoc) { - if (!issuerx509certificate) + if (!issuerx509certificate || issuerx509certificate.length === 0) throw new Error( - `issuerx509certificate is not provided for credential type ${OpenId4VciCredentialFormatProfile.MsoMdoc}`, + `issuerx509certificate is not provided or empty for credential type ${OpenId4VciCredentialFormatProfile.MsoMdoc}`, ) if (!credentialConfiguration.doctype) { @@ -205,7 +205,8 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent const payload = credentialPayload[0]?.payload const context = payload?.['@context'] const contextArray = Array.isArray(context) ? context : context ? [context] : [] - const isV2 = contextArray.includes(CREDENTIALS_CONTEXT_V2_URL) || !!payload?.validFrom + const isV2 = + contextArray.includes(CREDENTIALS_CONTEXT_V2_URL) || !!payload?.validFrom || !!payload?.validUntil return { format: ClaimFormat.JwtVc, From 68e1f2f58e13d0bf8e61be7203a0a9baa11ab368 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Mon, 15 Jun 2026 17:42:36 +0530 Subject: [PATCH 5/9] feat(dcql): add support for DCQL JSON-LD credential verification Signed-off-by: Sagar Khole --- .../openid4vc/types/verifier.types.ts | 41 ++++--- src/routes/routes.ts | 45 ++++++- src/routes/swagger.json | 110 +++++++++++++++--- 3 files changed, 158 insertions(+), 38 deletions(-) diff --git a/src/controllers/openid4vc/types/verifier.types.ts b/src/controllers/openid4vc/types/verifier.types.ts index 8ce745e9..0f87fbc3 100644 --- a/src/controllers/openid4vc/types/verifier.types.ts +++ b/src/controllers/openid4vc/types/verifier.types.ts @@ -34,26 +34,31 @@ export interface PresentationDefinition { /* DCQL MODELS */ /* -------------------------------------------------------------------------- */ -// export interface DcqlClaim { -// path: string[] -// intent_to_retain?: boolean -// } - -// export interface DcqlCredential { -// id: string -// format: string -// meta?: Record -// require_cryptographic_holder_binding?: boolean -// claims: DcqlClaim[] -// } - -// export interface DcqlQuery { -// combine?: 'all' | 'any' -// credentials: DcqlCredential[] -// } +export interface DcqlClaim { + path: string[] + intent_to_retain?: boolean +} + +export interface DcqlCredential { + id: string + format: string + meta?: Record + require_cryptographic_holder_binding?: boolean + claims?: DcqlClaim[] +} + +export interface DcqlCredentialSet { + options: string[][] + required?: boolean +} + +export interface DcqlQuery { + credentials: DcqlCredential[] + credential_sets?: DcqlCredentialSet[] +} export interface DcqlDefinition { - query: any + query: DcqlQuery } /* -------------------------------------------------------------------------- */ diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 87a6cd86..e1eaa8f0 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -570,10 +570,49 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DcqlClaim": { + "dataType": "refObject", + "properties": { + "path": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "intent_to_retain": {"dataType":"boolean"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DcqlCredential": { + "dataType": "refObject", + "properties": { + "id": {"dataType":"string","required":true}, + "format": {"dataType":"string","required":true}, + "meta": {"ref":"Record_string.any_"}, + "require_cryptographic_holder_binding": {"dataType":"boolean"}, + "claims": {"dataType":"array","array":{"dataType":"refObject","ref":"DcqlClaim"}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DcqlCredentialSet": { + "dataType": "refObject", + "properties": { + "options": {"dataType":"array","array":{"dataType":"array","array":{"dataType":"string"}},"required":true}, + "required": {"dataType":"boolean"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DcqlQuery": { + "dataType": "refObject", + "properties": { + "credentials": {"dataType":"array","array":{"dataType":"refObject","ref":"DcqlCredential"},"required":true}, + "credential_sets": {"dataType":"array","array":{"dataType":"refObject","ref":"DcqlCredentialSet"}}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "DcqlDefinition": { "dataType": "refObject", "properties": { - "query": {"dataType":"any","required":true}, + "query": {"ref":"DcqlQuery","required":true}, }, "additionalProperties": false, }, @@ -1474,7 +1513,7 @@ const models: TsoaRoute.Models = { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Pick_W3cCredential.Exclude_keyofW3cCredential.context-or-credentialSubject__": { "dataType": "refAlias", - "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"issuer":{"dataType":"union","subSchemas":[{"dataType":"string"},{"ref":"W3cIssuer"}],"required":true},"type":{"dataType":"array","array":{"dataType":"string"},"required":true},"id":{"dataType":"string"},"@context":{"dataType":"array","array":{"dataType":"union","subSchemas":[{"dataType":"string"},{"ref":"JsonObject"}]},"required":true},"issuanceDate":{"dataType":"string","required":true},"expirationDate":{"dataType":"string"},"credentialSchema":{"ref":"SingleOrArray_W3cCredentialSchema_"},"credentialStatus":{"ref":"W3cCredentialStatus"}},"validators":{}}, + "type": {"dataType":"nestedObjectLiteral","nestedProperties":{"issuer":{"dataType":"union","subSchemas":[{"dataType":"string"},{"ref":"W3cIssuer"}],"required":true},"type":{"dataType":"array","array":{"dataType":"string"},"required":true},"id":{"dataType":"string"},"issuanceDate":{"dataType":"string","required":true},"expirationDate":{"dataType":"string"},"credentialSchema":{"ref":"SingleOrArray_W3cCredentialSchema_"},"credentialStatus":{"ref":"W3cCredentialStatus"}},"validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "Omit_W3cCredential.context-or-credentialSubject_": { @@ -1606,7 +1645,7 @@ const models: TsoaRoute.Models = { "W3cJsonLdVerifiableCredential": { "dataType": "refObject", "properties": { - "@context": {"dataType":"array","array":{"dataType":"union","subSchemas":[{"dataType":"string"},{"ref":"JsonObject"}]},"required":true}, + "context": {"dataType":"array","array":{"dataType":"union","subSchemas":[{"dataType":"string"},{"ref":"JsonObject"}]},"required":true}, "id": {"dataType":"string"}, "type": {"dataType":"array","array":{"dataType":"string"},"required":true}, "issuer": {"dataType":"union","subSchemas":[{"dataType":"string"},{"ref":"W3cIssuer"}],"required":true}, diff --git a/src/routes/swagger.json b/src/routes/swagger.json index 3cd77384..b0ed70f1 100644 --- a/src/routes/swagger.json +++ b/src/routes/swagger.json @@ -1140,9 +1140,99 @@ "type": "object", "additionalProperties": false }, + "DcqlClaim": { + "properties": { + "path": { + "items": { + "type": "string" + }, + "type": "array" + }, + "intent_to_retain": { + "type": "boolean" + } + }, + "required": [ + "path" + ], + "type": "object", + "additionalProperties": false + }, + "DcqlCredential": { + "properties": { + "id": { + "type": "string" + }, + "format": { + "type": "string" + }, + "meta": { + "$ref": "#/components/schemas/Record_string.any_" + }, + "require_cryptographic_holder_binding": { + "type": "boolean" + }, + "claims": { + "items": { + "$ref": "#/components/schemas/DcqlClaim" + }, + "type": "array" + } + }, + "required": [ + "id", + "format" + ], + "type": "object", + "additionalProperties": false + }, + "DcqlCredentialSet": { + "properties": { + "options": { + "items": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "array" + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "options" + ], + "type": "object", + "additionalProperties": false + }, + "DcqlQuery": { + "properties": { + "credentials": { + "items": { + "$ref": "#/components/schemas/DcqlCredential" + }, + "type": "array" + }, + "credential_sets": { + "items": { + "$ref": "#/components/schemas/DcqlCredentialSet" + }, + "type": "array" + } + }, + "required": [ + "credentials" + ], + "type": "object", + "additionalProperties": false + }, "DcqlDefinition": { "properties": { - "query": {} + "query": { + "$ref": "#/components/schemas/DcqlQuery" + } }, "required": [ "query" @@ -3481,19 +3571,6 @@ "id": { "type": "string" }, - "@context": { - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/components/schemas/JsonObject" - } - ] - }, - "type": "array" - }, "issuanceDate": { "type": "string" }, @@ -3510,7 +3587,6 @@ "required": [ "issuer", "type", - "@context", "issuanceDate" ], "type": "object", @@ -3861,7 +3937,7 @@ }, "W3cJsonLdVerifiableCredential": { "properties": { - "@context": { + "context": { "items": { "anyOf": [ { @@ -3913,7 +3989,7 @@ } }, "required": [ - "@context", + "context", "type", "issuer", "issuanceDate", From 6b9c7850eb1a4bcd95d7ce18454964b9a6294ae8 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Wed, 17 Jun 2026 12:55:35 +0530 Subject: [PATCH 6/9] fix:import order and formatting in issuance service Signed-off-by: Sagar Khole --- .../openid4vc/issuance-sessions/issuance-sessions.service.ts | 4 +--- src/utils/oid4vc-agent.ts | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts b/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts index d0b32717..c5264798 100644 --- a/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts +++ b/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts @@ -1,10 +1,8 @@ import type { OpenId4VcIssuanceSessionsCreateOffer } from '../types/issuer.types' import type { Request as Req } from 'express' -import { type OpenId4VcIssuanceSessionState } from '@credo-ts/openid4vc' -import { OpenId4VcIssuanceSessionRepository } from '@credo-ts/openid4vc' - import { CREDENTIALS_CONTEXT_V1_URL, CREDENTIALS_CONTEXT_V2_URL } from '@credo-ts/core' +import { OpenId4VcIssuanceSessionRepository, type OpenId4VcIssuanceSessionState } from '@credo-ts/openid4vc' import { CredentialFormat, SignerMethod } from '../../../enums/enum' import { BadRequestError, NotFoundError } from '../../../errors/errors' diff --git a/src/utils/oid4vc-agent.ts b/src/utils/oid4vc-agent.ts index 208f06f7..38d568bb 100644 --- a/src/utils/oid4vc-agent.ts +++ b/src/utils/oid4vc-agent.ts @@ -205,8 +205,7 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent const payload = credentialPayload[0]?.payload const context = payload?.['@context'] const contextArray = Array.isArray(context) ? context : context ? [context] : [] - const isV2 = - contextArray.includes(CREDENTIALS_CONTEXT_V2_URL) || !!payload?.validFrom || !!payload?.validUntil + const isV2 = contextArray.includes(CREDENTIALS_CONTEXT_V2_URL) || !!payload?.validFrom || !!payload?.validUntil return { format: ClaimFormat.JwtVc, From d754b40a63326b3345e9ded189d7244bd52d5bf0 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Thu, 25 Jun 2026 14:41:13 +0530 Subject: [PATCH 7/9] feat(oid4vc):ldp_vc support for w3c jsonld Signed-off-by: Sagar Khole --- .../holder/credentialBindingResolver.ts | 1 + src/utils/oid4vc-agent.ts | 23 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/controllers/openid4vc/holder/credentialBindingResolver.ts b/src/controllers/openid4vc/holder/credentialBindingResolver.ts index 6df733dd..1f7ecdf2 100644 --- a/src/controllers/openid4vc/holder/credentialBindingResolver.ts +++ b/src/controllers/openid4vc/holder/credentialBindingResolver.ts @@ -104,6 +104,7 @@ export function getCredentialBindingResolver({ credentialFormat === OpenId4VciCredentialFormatProfile.SdJwtDc || credentialFormat === OpenId4VciCredentialFormatProfile.JwtVcJsonLd || credentialFormat === OpenId4VciCredentialFormatProfile.JwtVcJson || + credentialFormat === OpenId4VciCredentialFormatProfile.LdpVc || credentialFormat === OpenId4VciCredentialFormatProfile.MsoMdoc) ) { return { diff --git a/src/utils/oid4vc-agent.ts b/src/utils/oid4vc-agent.ts index 38d568bb..b20f3ff4 100644 --- a/src/utils/oid4vc-agent.ts +++ b/src/utils/oid4vc-agent.ts @@ -196,7 +196,8 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent if ( credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJson || - credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd + credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd || + credentialConfiguration.format === OpenId4VciCredentialFormatProfile.LdpVc ) { if (credential.signerOptions.method === SignerMethod.X5c) { throw new Error(`X5c signing method is not supported for W3C VC formats (${credentialConfiguration.format})`) @@ -208,7 +209,10 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent const isV2 = contextArray.includes(CREDENTIALS_CONTEXT_V2_URL) || !!payload?.validFrom || !!payload?.validUntil return { - format: ClaimFormat.JwtVc, + format: + credentialConfiguration.format === OpenId4VciCredentialFormatProfile.LdpVc + ? ClaimFormat.LdpVc + : ClaimFormat.JwtVc, credentials: holderBinding.keys.map((binding) => { let rawSubject: any let subjectId: string | undefined = undefined @@ -252,13 +256,17 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent if (isV2) { credentialJson.validFrom = payload.validFrom || payload.issuanceDate - credentialJson.validUntil = payload.validUntil || payload.expirationDate + if (payload.validUntil || payload.expirationDate) { + credentialJson.validUntil = payload.validUntil || payload.expirationDate + credentialJson.expirationDate = credentialJson.validUntil + } // Add issuanceDate for JWT signer compatibility credentialJson.issuanceDate = credentialJson.validFrom - credentialJson.expirationDate = credentialJson.validUntil } else { credentialJson.issuanceDate = payload.issuanceDate - credentialJson.expirationDate = payload.expirationDate + if (payload.expirationDate) { + credentialJson.expirationDate = payload.expirationDate + } } const credInstance: any = isV2 @@ -278,7 +286,10 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent } return { - format: ClaimFormat.JwtVc, + format: + credentialConfiguration.format === OpenId4VciCredentialFormatProfile.LdpVc + ? ClaimFormat.LdpVc + : ClaimFormat.JwtVc, verificationMethod: issuerDidVerificationMethod, credential: credInstance, } From 6c8c2a2ea4d9dbdfec9961e1f606bbca3ef24249 Mon Sep 17 00:00:00 2001 From: KambleSahil3 Date: Thu, 25 Jun 2026 15:17:28 +0530 Subject: [PATCH 8/9] chore: add env in docker compose Signed-off-by: KambleSahil3 --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 31bbbe49..e773a5f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: environment: # possible to set values using env variables AFJ_REST_LOG_LEVEL: 1 + env_file: + - .env volumes: # also possible to set values using json - ./samples/cliConfig.json:/config.json From 0501e55e8f548d9d2d39eee93ab3eb8a98aa3e0f Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Fri, 26 Jun 2026 07:46:11 +0530 Subject: [PATCH 9/9] fix:fixed sonar lint issue Signed-off-by: Sagar Khole --- .../issuance-sessions.service.ts | 90 ++-- .../openid4vc/types/issuer.types.ts | 2 +- .../verification-sessions.service.ts | 82 +-- src/utils/oid4vc-agent.ts | 484 ++++++++++-------- 4 files changed, 381 insertions(+), 277 deletions(-) diff --git a/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts b/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts index c5264798..72c7eb8b 100644 --- a/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts +++ b/src/controllers/openid4vc/issuance-sessions/issuance-sessions.service.ts @@ -95,15 +95,19 @@ class IssuanceSessionsService { ) } - if (version === 'v2.0') { - if (cred.payload.issuer) { - const issuer = cred.payload.issuer - if (typeof issuer === 'object' && !issuer.id) { - throw new BadRequestError(`Issuer object for '${cred.credentialSupportedId}' must contain 'id' property`) - } - } + if ( + version === 'v2.0' && + cred.payload?.issuer && + typeof cred.payload.issuer === 'object' && + !cred.payload.issuer.id + ) { + throw new BadRequestError(`Issuer object for '${cred.credentialSupportedId}' must contain 'id' property`) } + this.validateSignerOptions(cred) + } + + private validateSignerOptions(cred: any) { if (!cred.signerOptions?.method) { throw new BadRequestError( `signerOptions must be provided and allowed methods are ${Object.values(SignerMethod).join(', ')}`, @@ -130,21 +134,6 @@ class IssuanceSessionsService { const transformed = { ...payload } - const formatDate = (date: any) => { - if (!date) return undefined - if (date instanceof Date) return date.toISOString() - if (typeof date === 'string') { - try { - const d = new Date(date) - if (isNaN(d.getTime())) return date - return d.toISOString() - } catch { - return date - } - } - return date - } - // Rule: issuanceDate -> validFrom if (transformed.issuanceDate && !transformed.validFrom) { transformed.validFrom = transformed.issuanceDate @@ -157,44 +146,61 @@ class IssuanceSessionsService { } // Normalize dates to ISO format - if (transformed.validFrom) transformed.validFrom = formatDate(transformed.validFrom) - if (transformed.validUntil) transformed.validUntil = formatDate(transformed.validUntil) + if (transformed.validFrom) transformed.validFrom = this.formatDate(transformed.validFrom) + if (transformed.validUntil) transformed.validUntil = this.formatDate(transformed.validUntil) // Rule: issuer string -> object (standardizing for v2.0 if it is a DID) if (typeof transformed.issuer === 'string' && transformed.issuer.startsWith('did:')) { transformed.issuer = { id: transformed.issuer } } - // Rule: Update @context for v2.0 + this.updateContextForVersion(transformed, version) + + return transformed + } + + private formatDate(date: any): any { + if (!date) return undefined + if (date instanceof Date) return date.toISOString() + if (typeof date === 'string') { + try { + const d = new Date(date) + if (Number.isNaN(d.getTime())) return date + return d.toISOString() + } catch { + return date + } + } + return date + } + + private updateContextForVersion(transformed: any, version: 'v1.1' | 'v2.0' | undefined) { const v1Context = CREDENTIALS_CONTEXT_V1_URL const v2Context = CREDENTIALS_CONTEXT_V2_URL if (version === 'v2.0') { - const currentCtx = Array.isArray(transformed['@context']) - ? transformed['@context'] - : typeof transformed['@context'] === 'string' - ? [transformed['@context']] - : [] + let currentCtx: any[] = [] + if (Array.isArray(transformed['@context'])) { + currentCtx = transformed['@context'] + } else if (typeof transformed['@context'] === 'string') { + currentCtx = [transformed['@context']] + } const ctxSet = new Set(currentCtx) ctxSet.delete(v1Context) ctxSet.delete(v2Context) // W3C V2.0 requires the V2 context to be the very first element. transformed['@context'] = [v2Context, v1Context, ...Array.from(ctxSet)] - } else { + } else if (!transformed['@context']) { // W3C V1.1 / Default behavior - if (!transformed['@context']) { - transformed['@context'] = [v1Context] - } else if (Array.isArray(transformed['@context'])) { - const ctxSet = new Set(transformed['@context']) - ctxSet.delete(v1Context) - transformed['@context'] = [v1Context, ...Array.from(ctxSet)] - } else if (typeof transformed['@context'] === 'string') { - transformed['@context'] = [v1Context, transformed['@context']] - } + transformed['@context'] = [v1Context] + } else if (Array.isArray(transformed['@context'])) { + const ctxSet = new Set(transformed['@context']) + ctxSet.delete(v1Context) + transformed['@context'] = [v1Context, ...Array.from(ctxSet)] + } else if (typeof transformed['@context'] === 'string') { + transformed['@context'] = [v1Context, transformed['@context']] } - - return transformed } private async processStatusList( diff --git a/src/controllers/openid4vc/types/issuer.types.ts b/src/controllers/openid4vc/types/issuer.types.ts index 0ff32bf9..14d158c9 100644 --- a/src/controllers/openid4vc/types/issuer.types.ts +++ b/src/controllers/openid4vc/types/issuer.types.ts @@ -1,5 +1,5 @@ import type { SignerMethod } from '../../../enums/enum' -import type { MdocNameSpaces, W3cCredential } from '@credo-ts/core' +import type { MdocNameSpaces } from '@credo-ts/core' import type { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' export interface OpenId4VciOfferCredentials { diff --git a/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts b/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts index 5b0a8214..b24069f9 100644 --- a/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts +++ b/src/controllers/openid4vc/verifier-sessions/verification-sessions.service.ts @@ -25,12 +25,40 @@ export class VerificationSessionsService { const verifier = agentReq.agent.modules.openid4vc.verifier if (!verifier) throw new Error('OID4VC verifier module not initialized') - let requestSigner - let parsedCertificate - if (dto.requestSigner.method === SignerMethod.Did) { - requestSigner = dto.requestSigner as OpenId4VcJwtIssuerDid + const { requestSigner, parsedCertificate } = await this.resolveRequestSigner(agentReq, dto.requestSigner) + const options: any = { + requestSigner, + verifierId: dto.verifierId, + version: dto.version, + } + + if (dto.responseMode === ResponseModeEnum.DC_API || dto.responseMode === ResponseModeEnum.DC_API_JWT) { + options.expectedOrigins = dto.expectedOrigins + } - const didToResolve = dto.requestSigner?.didUrl + this.validatePresentationDetails(dto) + + if (dto.responseMode) options.responseMode = dto.responseMode + if (dto.presentationExchange) { + options.presentationExchange = dto.presentationExchange + } else { + options.dcql = dto.dcql + } + + if (parsedCertificate) { + parsedCertificate.publicJwk.keyId = requestSigner.keyId + options.requestSigner.x5c = [parsedCertificate] + } + return (await verifier.createAuthorizationRequest(options)) as any + } + + private async resolveRequestSigner( + agentReq: Req, + requestSignerDto: CreateAuthorizationRequest['requestSigner'], + ): Promise<{ requestSigner: any; parsedCertificate?: any }> { + if (requestSignerDto.method === SignerMethod.Did) { + const requestSigner = requestSignerDto + const didToResolve = requestSignerDto.didUrl if (!didToResolve) { throw new Error('No DID provided to resolve (neither requestSigner.didUrl nor verifierDid present)') } @@ -50,45 +78,31 @@ export class VerificationSessionsService { requestSigner.didUrl = verifierDidUrl } - requestSigner = { method: 'did', didUrl: verifierDidUrl } as any - } else { - requestSigner = dto.requestSigner as OpenId4VcIssuerX5cOptions - - parsedCertificate = X509Service.parseCertificate(agentReq.agent.context, { - encodedCertificate: requestSigner.x5c[0], - }) - requestSigner.issuer = parsedCertificate.sanUriNames[0] - requestSigner.clientIdPrefix = dto.requestSigner.clientIdPrefix ?? ClientIdPrefix.X509Hash - } - const options: any = { - requestSigner, - verifierId: dto.verifierId, - version: dto.version, + return { + requestSigner: { method: 'did', didUrl: verifierDidUrl }, + } } - if (dto.responseMode === ResponseModeEnum.DC_API || dto.responseMode === ResponseModeEnum.DC_API_JWT) { - options.expectedOrigins = dto.expectedOrigins + const requestSigner = requestSignerDto as OpenId4VcIssuerX5cOptions + const parsedCertificate = X509Service.parseCertificate(agentReq.agent.context, { + encodedCertificate: requestSigner.x5c[0], + }) + requestSigner.issuer = parsedCertificate.sanUriNames[0] + requestSigner.clientIdPrefix = requestSigner.clientIdPrefix ?? ClientIdPrefix.X509Hash + + return { + requestSigner, + parsedCertificate, } + } + private validatePresentationDetails(dto: CreateAuthorizationRequest) { if (dto.presentationExchange && dto.dcql) { throw new Error('Only one of presentationExchange or dcql can be provided, not both') } if (!dto.presentationExchange && !dto.dcql) { throw new Error('Either presentationExchange or dcql must be provided') } - - if (dto.responseMode) options.responseMode = dto.responseMode - if (dto.presentationExchange) { - options.presentationExchange = dto.presentationExchange - } else { - options.dcql = dto.dcql - } - - if (parsedCertificate) { - parsedCertificate.publicJwk.keyId = requestSigner.keyId - options.requestSigner.x5c = [parsedCertificate] - } - return (await verifier.createAuthorizationRequest(options)) as any } public async findVerificationSessionsByQuery( diff --git a/src/utils/oid4vc-agent.ts b/src/utils/oid4vc-agent.ts index b20f3ff4..6fbf7938 100644 --- a/src/utils/oid4vc-agent.ts +++ b/src/utils/oid4vc-agent.ts @@ -85,113 +85,30 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent } if (credentialConfigurationId === 'PresentationAuthorization') { - const trustedCertificates = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates - if (trustedCertificates?.length !== 1) { - throw new Error(`Expected exactly one trusted certificate. Received ${trustedCertificates?.length}.`) - } - - return { - format: ClaimFormat.SdJwtDc, - credentials: [ - { - payload: { - vct: credentialConfiguration.vct as string, - authorized_user: authorization.accessToken.payload.sub, - }, - holder: { - method: 'jwk', - jwk: holderBinding.keys[0].jwk, - } as SdJwtVcHolderBinding, - issuer: { - method: 'x5c', - x5c: trustedCertificates.map((cert) => X509Certificate.fromEncodedCertificate(cert)), - issuer: 'ISSUER_HOST', - }, - }, - ], - type: 'credentials', - } satisfies OpenId4VciSignSdJwtCredentials + return handlePresentationAuthorization(agentContext, credentialConfiguration, authorization, holderBinding) } if (credentialConfiguration.format === OpenId4VciCredentialFormatProfile.MsoMdoc) { - if (!issuerx509certificate || issuerx509certificate.length === 0) - throw new Error( - `issuerx509certificate is not provided or empty for credential type ${OpenId4VciCredentialFormatProfile.MsoMdoc}`, - ) - - if (!credentialConfiguration.doctype) { - throw new Error(`'doctype' not found in credential configuration,`) - } - - const parsedCertificates = issuerx509certificate.map((cert) => { - return X509Service.parseCertificate(agentContext, { - encodedCertificate: cert, - }) - }) - - parsedCertificates[0].publicJwk.keyId = credential.signerOptions.keyId - const updatedNamespaces = processIsoImages(credential.payload.namespaces) - credential.payload.namespaces = updatedNamespaces - return { - type: 'credentials', - format: ClaimFormat.MsoMdoc, - credentials: holderBinding.keys.map((holderBindingDetails) => ({ - issuerCertificate: parsedCertificates as any, - holderKey: holderBindingDetails.jwk, - ...credential.payload, - validityInfo: { - signed: credential.payload.validityInfo.signed - ? new Date(credential.payload.validityInfo.signed) - : new Date(), - validFrom: new Date(credential.payload.validityInfo.validFrom), - validUntil: new Date(credential.payload.validityInfo.validUntil), - }, - docType: credentialConfiguration.doctype, - })), - } satisfies OpenId4VciSignMdocCredentials + return handleMsoMdocFormat( + agentContext, + credentialConfiguration, + credential, + holderBinding, + issuerx509certificate, + ) } + if (credentialConfiguration.format === OpenId4VciCredentialFormatProfile.SdJwtDc) { - const disclosureFramePayload = - credential.disclosureFrame && Object.keys(credential.disclosureFrame).length > 0 - ? credential.disclosureFrame - : {} - //Taking leaf certifcate from chain as issuer certificate, if not provided explicitly taking AGENT_HTTP_URL as issuer - let parsedCertificate: any - if (!issuerDidVerificationMethod && issuerx509certificate && issuerx509certificate.length > 0) { - parsedCertificate = X509Service.parseCertificate(agentContext, { - encodedCertificate: issuerx509certificate[0], - }) - parsedCertificate.publicJwk.keyId = credential.signerOptions.keyId - } else if (!issuerDidVerificationMethod) { - throw new Error(`issuerx509certificate is not provided for credential ${credentialConfigurationId}`) - } - return { - format: ClaimFormat.SdJwtDc, - credentials: holderBinding.keys.map((binding) => ({ - payload: credentialPayload[0]?.payload, - holder: - binding.method === 'did' - ? ({ - method: 'did' as const, - didUrl: binding.didUrl, - } as SdJwtVcHolderBinding) - : ({ - method: 'jwk' as const, - jwk: binding.method === 'jwk' ? binding.jwk : {}, - } as SdJwtVcHolderBinding), - issuer: issuerDidVerificationMethod - ? { - method: 'did', - didUrl: issuerDidVerificationMethod, - } - : { - method: 'x5c', - x5c: [parsedCertificate], - }, - disclosureFrame: disclosureFramePayload, - })), - type: 'credentials', - } satisfies OpenId4VciSignSdJwtCredentials + return handleSdJwtDcFormat( + agentContext, + credentialConfiguration, + credentialConfigurationId, + credential, + credentialPayload, + holderBinding, + issuerDidVerificationMethod, + issuerx509certificate, + ) } if ( @@ -199,103 +116,13 @@ export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredent credentialConfiguration.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd || credentialConfiguration.format === OpenId4VciCredentialFormatProfile.LdpVc ) { - if (credential.signerOptions.method === SignerMethod.X5c) { - throw new Error(`X5c signing method is not supported for W3C VC formats (${credentialConfiguration.format})`) - } - - const payload = credentialPayload[0]?.payload - const context = payload?.['@context'] - const contextArray = Array.isArray(context) ? context : context ? [context] : [] - const isV2 = contextArray.includes(CREDENTIALS_CONTEXT_V2_URL) || !!payload?.validFrom || !!payload?.validUntil - - return { - format: - credentialConfiguration.format === OpenId4VciCredentialFormatProfile.LdpVc - ? ClaimFormat.LdpVc - : ClaimFormat.JwtVc, - credentials: holderBinding.keys.map((binding) => { - let rawSubject: any - let subjectId: string | undefined = undefined - const bindingDid = binding.method === 'did' ? (binding as any).didUrl : undefined - - if (Array.isArray(payload.credentialSubject)) { - rawSubject = payload.credentialSubject.map((subj: any) => { - if (subj && typeof subj === 'object') { - const itemId = subj.id || bindingDid - return itemId ? { ...subj, id: itemId } : { ...subj } - } - return subj - }) - const firstWithId = rawSubject.find((s: any) => s?.id) - subjectId = firstWithId?.id || bindingDid - } else { - const origSubject = payload.credentialSubject || {} - subjectId = origSubject.id || bindingDid - - rawSubject = { ...origSubject } - if (subjectId) { - rawSubject.id = subjectId - } - } - - const issuer = payload.issuer || credential.signerOptions.did - const finalIssuer = isV2 && typeof issuer === 'string' ? { id: issuer } : issuer - - const mainContext = isV2 ? CREDENTIALS_CONTEXT_V2_URL : CREDENTIALS_CONTEXT_V1_URL - - const finalContext = contextArray.includes(mainContext) - ? [mainContext, ...contextArray.filter((c) => c !== mainContext)] - : [mainContext, ...contextArray] - - const credentialJson: any = { - '@context': finalContext, - type: payload.type, - issuer: finalIssuer, - credentialSubject: rawSubject, - } - - if (isV2) { - credentialJson.validFrom = payload.validFrom || payload.issuanceDate - if (payload.validUntil || payload.expirationDate) { - credentialJson.validUntil = payload.validUntil || payload.expirationDate - credentialJson.expirationDate = credentialJson.validUntil - } - // Add issuanceDate for JWT signer compatibility - credentialJson.issuanceDate = credentialJson.validFrom - } else { - credentialJson.issuanceDate = payload.issuanceDate - if (payload.expirationDate) { - credentialJson.expirationDate = payload.expirationDate - } - } - - const credInstance: any = isV2 - ? JsonTransformer.fromJSON(credentialJson, W3cV2Credential) - : JsonTransformer.fromJSON(credentialJson, W3cCredential) - - if (credInstance.credentialSubject) { - if (Array.isArray(credInstance.credentialSubject)) { - for (const item of credInstance.credentialSubject) { - if (item && typeof item === 'object') { - item.id = item.id || subjectId - } - } - } else { - credInstance.credentialSubject.id = subjectId - } - } - - return { - format: - credentialConfiguration.format === OpenId4VciCredentialFormatProfile.LdpVc - ? ClaimFormat.LdpVc - : ClaimFormat.JwtVc, - verificationMethod: issuerDidVerificationMethod, - credential: credInstance, - } - }), - type: 'credentials', - } as any + return handleW3cCredentialFormat( + credentialConfiguration, + credential, + credentialPayload[0]?.payload, + holderBinding, + issuerDidVerificationMethod, + ) as any } throw new Error('Invalid request format ' + credentialConfiguration.format) @@ -345,7 +172,7 @@ export interface OpenId4VcIssuanceSessionCreateOfferSdJwtCredentialOptions { */ payload: { vct?: string - issuer?: string | any + issuer?: any credentialSubject?: any validFrom?: string validUntil?: string @@ -451,4 +278,261 @@ export async function getX509CertsByUrl(): Promise { return data as string[] } +function handlePresentationAuthorization( + agentContext: any, + credentialConfiguration: any, + authorization: any, + holderBinding: any, +) { + const trustedCertificates = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates + if (trustedCertificates?.length !== 1) { + throw new Error(`Expected exactly one trusted certificate. Received ${trustedCertificates?.length}.`) + } + + return { + format: ClaimFormat.SdJwtDc, + credentials: [ + { + payload: { + vct: credentialConfiguration.vct as string, + authorized_user: authorization.accessToken.payload.sub, + }, + holder: { + method: 'jwk', + jwk: holderBinding.keys[0].jwk, + } as SdJwtVcHolderBinding, + issuer: { + method: 'x5c', + x5c: trustedCertificates.map((cert: any) => X509Certificate.fromEncodedCertificate(cert)), + issuer: 'ISSUER_HOST', + }, + }, + ], + type: 'credentials', + } satisfies OpenId4VciSignSdJwtCredentials +} + +function handleMsoMdocFormat( + agentContext: any, + credentialConfiguration: any, + credential: any, + holderBinding: any, + issuerx509certificate?: string[], +) { + if (!issuerx509certificate || issuerx509certificate.length === 0) + throw new Error( + `issuerx509certificate is not provided or empty for credential type ${OpenId4VciCredentialFormatProfile.MsoMdoc}`, + ) + + if (!credentialConfiguration.doctype) { + throw new Error(`'doctype' not found in credential configuration,`) + } + + const parsedCertificates = issuerx509certificate.map((cert) => { + return X509Service.parseCertificate(agentContext, { + encodedCertificate: cert, + }) + }) + + parsedCertificates[0].publicJwk.keyId = credential.signerOptions.keyId + const updatedNamespaces = processIsoImages(credential.payload.namespaces) + credential.payload.namespaces = updatedNamespaces + return { + type: 'credentials', + format: ClaimFormat.MsoMdoc, + credentials: holderBinding.keys.map((holderBindingDetails: any) => ({ + issuerCertificate: parsedCertificates as any, + holderKey: holderBindingDetails.jwk, + ...credential.payload, + validityInfo: { + signed: credential.payload.validityInfo.signed ? new Date(credential.payload.validityInfo.signed) : new Date(), + validFrom: new Date(credential.payload.validityInfo.validFrom), + validUntil: new Date(credential.payload.validityInfo.validUntil), + }, + docType: credentialConfiguration.doctype, + })), + } satisfies OpenId4VciSignMdocCredentials +} + +function handleSdJwtDcFormat( + agentContext: any, + credentialConfiguration: any, + credentialConfigurationId: string, + credential: any, + credentialPayload: any, + holderBinding: any, + issuerDidVerificationMethod?: string, + issuerx509certificate?: string[], +) { + const disclosureFramePayload = + credential.disclosureFrame && Object.keys(credential.disclosureFrame).length > 0 ? credential.disclosureFrame : {} + //Taking leaf certifcate from chain as issuer certificate, if not provided explicitly taking AGENT_HTTP_URL as issuer + let parsedCertificate: any + if (!issuerDidVerificationMethod && issuerx509certificate && issuerx509certificate.length > 0) { + parsedCertificate = X509Service.parseCertificate(agentContext, { + encodedCertificate: issuerx509certificate[0], + }) + parsedCertificate.publicJwk.keyId = credential.signerOptions.keyId + } else if (!issuerDidVerificationMethod) { + throw new Error(`issuerx509certificate is not provided for credential ${credentialConfigurationId}`) + } + return { + format: ClaimFormat.SdJwtDc, + credentials: holderBinding.keys.map((binding: any) => ({ + payload: credentialPayload[0]?.payload, + holder: + binding.method === 'did' + ? ({ + method: 'did' as const, + didUrl: binding.didUrl, + } as SdJwtVcHolderBinding) + : ({ + method: 'jwk' as const, + jwk: binding.method === 'jwk' ? binding.jwk : {}, + } as SdJwtVcHolderBinding), + issuer: issuerDidVerificationMethod + ? { + method: 'did', + didUrl: issuerDidVerificationMethod, + } + : { + method: 'x5c', + x5c: [parsedCertificate], + }, + disclosureFrame: disclosureFramePayload, + })), + type: 'credentials', + } satisfies OpenId4VciSignSdJwtCredentials +} + +function handleW3cCredentialFormat( + credentialConfiguration: any, + credential: any, + payload: any, + holderBinding: any, + issuerDidVerificationMethod: string | undefined, +) { + if (credential.signerOptions.method === SignerMethod.X5c) { + throw new Error(`X5c signing method is not supported for W3C VC formats (${credentialConfiguration.format})`) + } + + const context = payload?.['@context'] + let contextArray: any[] = [] + if (Array.isArray(context)) { + contextArray = context + } else if (context) { + contextArray = [context] + } + const isV2 = contextArray.includes(CREDENTIALS_CONTEXT_V2_URL) || !!payload?.validFrom || !!payload?.validUntil + + return { + format: + credentialConfiguration.format === OpenId4VciCredentialFormatProfile.LdpVc + ? ClaimFormat.LdpVc + : ClaimFormat.JwtVc, + credentials: holderBinding.keys.map((binding: any) => + mapW3cCredentialBinding( + binding, + payload, + credential, + contextArray, + isV2, + issuerDidVerificationMethod, + credentialConfiguration, + ), + ), + type: 'credentials', + } +} + +function mapW3cCredentialBinding( + binding: any, + payload: any, + credential: any, + contextArray: any[], + isV2: boolean, + issuerDidVerificationMethod: string | undefined, + credentialConfiguration: any, +) { + let rawSubject: any + let subjectId: string | undefined = undefined + const bindingDid = binding.method === 'did' ? (binding as any).didUrl : undefined + + if (Array.isArray(payload.credentialSubject)) { + rawSubject = payload.credentialSubject.map((subj: any) => { + if (subj && typeof subj === 'object') { + const itemId = subj.id || bindingDid + return itemId ? { ...subj, id: itemId } : { ...subj } + } + return subj + }) + const firstWithId = rawSubject.find((s: any) => s?.id) + subjectId = firstWithId?.id || bindingDid + } else { + const origSubject = payload.credentialSubject || {} + subjectId = origSubject.id || bindingDid + + rawSubject = { ...origSubject } + if (subjectId) { + rawSubject.id = subjectId + } + } + + const issuer = payload.issuer || credential.signerOptions.did + const finalIssuer = isV2 && typeof issuer === 'string' ? { id: issuer } : issuer + + const mainContext = isV2 ? CREDENTIALS_CONTEXT_V2_URL : CREDENTIALS_CONTEXT_V1_URL + + const finalContext = contextArray.includes(mainContext) + ? [mainContext, ...contextArray.filter((c) => c !== mainContext)] + : [mainContext, ...contextArray] + + const credentialJson: any = { + '@context': finalContext, + type: payload.type, + issuer: finalIssuer, + credentialSubject: rawSubject, + } + + if (isV2) { + credentialJson.validFrom = payload.validFrom || payload.issuanceDate + if (payload.validUntil || payload.expirationDate) { + credentialJson.validUntil = payload.validUntil || payload.expirationDate + credentialJson.expirationDate = credentialJson.validUntil + } + // Add issuanceDate for JWT signer compatibility + credentialJson.issuanceDate = credentialJson.validFrom + } else { + credentialJson.issuanceDate = payload.issuanceDate + if (payload.expirationDate) { + credentialJson.expirationDate = payload.expirationDate + } + } + + const credInstance: any = isV2 + ? JsonTransformer.fromJSON(credentialJson, W3cV2Credential) + : JsonTransformer.fromJSON(credentialJson, W3cCredential) + + if (credInstance.credentialSubject) { + if (Array.isArray(credInstance.credentialSubject)) { + for (const item of credInstance.credentialSubject) { + if (item && typeof item === 'object') { + item.id = item.id || subjectId + } + } + } else { + credInstance.credentialSubject.id = subjectId + } + } + + return { + format: + credentialConfiguration.format === OpenId4VciCredentialFormatProfile.LdpVc + ? ClaimFormat.LdpVc + : ClaimFormat.JwtVc, + verificationMethod: issuerDidVerificationMethod, + credential: credInstance, + } +} + export { validateAuthConfig }