From f268d4825b49625877a266e34c4a98e46068e738 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Wed, 20 May 2026 15:44:14 +0530 Subject: [PATCH 01/15] feat(oid4vc): add support for jwt_vc_json-ld credential format Signed-off-by: Sagar Khole --- .../dtos/issuer-sessions.dto.ts | 6 +- .../dtos/oid4vc-issuer-template.dto.ts | 9 +- .../oid4vc-issuer-sessions.interfaces.ts | 3 +- .../helpers/credential-sessions.builder.ts | 109 +++++++++++++++++- .../libs/helpers/issuer.metadata.ts | 47 +++++++- libs/enum/src/enum.ts | 3 +- 6 files changed, 161 insertions(+), 16 deletions(-) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts index 32e5e34ab..bb29994f2 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts @@ -274,11 +274,13 @@ export class CredentialDto { @ApiProperty({ description: 'Credential format type', - enum: ['mso_mdoc', 'vc+sd-jwt'], + enum: ['mso_mdoc', 'vc+sd-jwt', 'jwt_vc_json-ld'], example: 'mso_mdoc' }) @IsString() - @IsIn(['mso_mdoc', 'vc+sd-jwt'], { message: 'format must be either "mso_mdoc" or "vc+sd-jwt"' }) + @IsIn(['mso_mdoc', 'vc+sd-jwt', 'jwt_vc_json-ld'], { + message: 'format must be either "mso_mdoc", "vc+sd-jwt" or "jwt_vc_json-ld"' + }) format: string; @ApiProperty({ diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts index a4300ee2a..b8ee8e5ca 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts @@ -229,8 +229,11 @@ export class CreateCredentialTemplateDto { @IsEnum(CredentialFormat) format: CredentialFormat; - @ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.SdJwtVc === o.format) - @IsEmpty({ message: 'doctype must not be provided when format is "dc+sd-jwt"' }) + @ValidateIf( + (o: CreateCredentialTemplateDto) => + CredentialFormat.SdJwtVc === o.format || CredentialFormat.JwtVcJsonLd === o.format + ) + @IsEmpty({ message: 'doctype must not be provided when format is "dc+sd-jwt" or "jwt_vc_json-ld"' }) readonly _doctypeAbsentGuard?: unknown; @ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.Mdoc === o.format) @@ -246,7 +249,7 @@ export class CreateCredentialTemplateDto { @Type(({ object }) => { if (object.format === CredentialFormat.Mdoc) { return MdocTemplateDto; - } else if (object.format === CredentialFormat.SdJwtVc) { + } else if (object.format === CredentialFormat.SdJwtVc || object.format === CredentialFormat.JwtVcJsonLd) { return SdJwtTemplateDto; } }) diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts index 9de39a0a8..a9334383e 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts @@ -5,7 +5,8 @@ import { OpenId4VcIssuanceSessionState } from '@credebl/enum/enum'; * --------------------------------------------------------- */ export enum CredentialFormat { SdJwtVc = 'vc+sd-jwt', - MsoMdoc = 'mso_mdoc' + MsoMdoc = 'mso_mdoc', + JwtVcJsonLd = 'jwt_vc_json-ld' } export enum SignerMethodOption { diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 3fee5d82a..6b187ba07 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -150,14 +150,23 @@ function mapDbFormatToApiFormat(dbFormat: string): CredentialFormat { if (['sd-jwt', 'dc+sd-jwt', 'vc+sd-jwt', 'sdjwt', 'sd+jwt-vc'].includes(normalized)) { return CredentialFormat.SdJwtVc; } + if (['jwt_vc_json-ld', 'jwt-vc-json-ld', 'w3c-jwt-json-ld'].includes(normalized)) { + return CredentialFormat.JwtVcJsonLd; + } if ('mso_mdoc' === normalized || 'mso-mdoc' === normalized || 'mdoc' === normalized) { return CredentialFormat.Mdoc; } throw new UnprocessableEntityException(`Unsupported template format: ${dbFormat}`); } -function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' { - return apiFormat === CredentialFormat.SdJwtVc ? 'sdjwt' : 'mdoc'; +function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' | 'jwt-vc-json-ld' { + if (apiFormat === CredentialFormat.SdJwtVc) { + return 'sdjwt'; + } + if (apiFormat === CredentialFormat.JwtVcJsonLd) { + return 'jwt-vc-json-ld'; + } + return 'mdoc'; } export function buildCredentialOfferUrl(baseUrl: string, getAllCredentialOffer: GetAllCredentialOffer): string { @@ -213,7 +222,7 @@ export function validatePayloadAgainstTemplate(template: any, payload: any): { v } }; - if (CredentialFormat.SdJwtVc === template.format) { + if (CredentialFormat.SdJwtVc === template.format || CredentialFormat.JwtVcJsonLd === template.format) { validateAttributes((template.attributes as SdJwtTemplate).attributes ?? [], payload); } else if (CredentialFormat.Mdoc === template.format) { const namespaces = payload?.namespaces; @@ -456,6 +465,97 @@ function buildMdocCredential( }; } +function buildJwtVcJsonLdCredential( + credentialRequest: CredentialRequestDtoLike, + templateRecord: CredentialTemplateRecord, + signerOptions: ISignerOption[], + activeCertificateDetails?: X509CertificateRecord[] +): BuiltCredential { + const payloadCopy = { ...(credentialRequest.payload as Record) }; + + let expectedSignerMethod: SignerMethodOption; + if (templateRecord.signerOption === SignerOption.DID) { + expectedSignerMethod = SignerMethodOption.DID; + } else if ( + templateRecord.signerOption === SignerOption.X509_P256 || + templateRecord.signerOption === SignerOption.X509_ED25519 + ) { + expectedSignerMethod = SignerMethodOption.X5C; + } else { + throw new UnprocessableEntityException( + `Unknown signer option "${templateRecord.signerOption}" for template ${templateRecord.id}` + ); + } + + const templateSignerOption: ISignerOption | undefined = signerOptions?.find((x) => x.method === expectedSignerMethod); + if (!templateSignerOption) { + throw new UnprocessableEntityException( + `Signer option "${expectedSignerMethod}" is not configured for template ${templateRecord.id}` + ); + } + + if (expectedSignerMethod === SignerMethodOption.X5C && credentialRequest.validityInfo) { + if (!activeCertificateDetails?.length) { + throw new UnprocessableEntityException('Active x.509 certificate details are required for x5c signer templates.'); + } + const certificateDetail = activeCertificateDetails.find( + (x) => x.certificateBase64 === templateSignerOption.x5c?.[0] + ); + if (!certificateDetail) { + throw new UnprocessableEntityException('No active x.509 certificate matches the configured signer option.'); + } + + const validationResult = validateCredentialDatesInCertificateWindow( + credentialRequest.validityInfo, + certificateDetail + ); + if (!validationResult.isValid) { + throw new UnprocessableEntityException(`${JSON.stringify(validationResult.details)}`); + } + } + + let nbf: number | undefined; + let exp: number | undefined; + + if (credentialRequest.validityInfo) { + const credentialValidFrom = new Date(credentialRequest.validityInfo.validFrom); + const credentialValidTo = new Date(credentialRequest.validityInfo.validUntil); + const isCredentialDurationValid = credentialValidFrom <= credentialValidTo; + if (!isCredentialDurationValid) { + const errorDetails = { + credentialDurationValid: isCredentialDurationValid, + credentialValidFrom: credentialValidFrom.toISOString(), + credentialValidTo: credentialValidTo.toISOString() + }; + throw new UnprocessableEntityException(`${JSON.stringify(errorDetails)}`); + } + nbf = dateToSeconds(credentialValidFrom); + exp = dateToSeconds(credentialValidTo); + } + + const apiFormat = mapDbFormatToApiFormat(templateRecord.format); + const idSuffix = formatSuffix(apiFormat); + const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + + const wrappedPayload: Record = { + credentialSubject: payloadCopy + }; + + if (nbf) { + wrappedPayload.nbf = nbf; + } + if (exp) { + wrappedPayload.exp = exp; + } + + return { + credentialSupportedId, + signerOptions: templateSignerOption ? templateSignerOption : undefined, + format: apiFormat, + payload: wrappedPayload + }; +} + export function buildCredentialOfferPayload( dto: CreateOidcCredentialOfferDtoLike, templates: credential_templates[], @@ -489,6 +589,9 @@ export function buildCredentialOfferPayload( if (apiFormat === CredentialFormat.SdJwtVc) { return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); } + if (apiFormat === CredentialFormat.JwtVcJsonLd) { + return buildJwtVcJsonLdCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); + } if (apiFormat === CredentialFormat.Mdoc) { return buildMdocCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); } diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index 9a23223e3..1aafb7d09 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -266,9 +266,7 @@ function buildClaimsFromTemplate(template: SdJwtTemplate | MdocTemplate): Claim[ return claims; } -//TODO: Fix this eslint issue -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate) { +export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate): Record { const formatSuffix = 'sdjwt'; // Determine the unique key for this credential configuration @@ -297,8 +295,7 @@ export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate }; } -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildMdocCredentialConfig(name: string, template: MdocTemplate) { +export function buildMdocCredentialConfig(name: string, template: MdocTemplate): Record { //const claims: Claim[] = []; const formatSuffix = 'mdoc'; @@ -332,12 +329,46 @@ export function buildMdocCredentialConfig(name: string, template: MdocTemplate) }; } +export function buildJwtVcJsonLdCredentialConfig( + name: string, + template: SdJwtTemplate +): Record { + const formatSuffix = 'jwt-vc-json-ld'; + + // Determine the unique key for this credential configuration + const configKey = `${name}-${formatSuffix}`; + const credentialScope = `openid4vc:${template.vct}-${formatSuffix}`; + + const claims = buildClaimsFromTemplate(template); + + return { + [configKey]: { + format: CredentialFormat.JwtVcJsonLd, + scope: credentialScope, + vct: template.vct, + credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS_FOR_SDJWT], + cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS_FOR_SDJWT], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: ['ES256', 'EdDSA'] + } + }, + credential_metadata: { + claims, + display: [] + } + } + }; +} + //TODO: Fix this eslint issue // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function buildCredentialConfig(name: string, template: SdJwtTemplate | MdocTemplate, format: CredentialFormat) { switch (format) { case CredentialFormat.SdJwtVc: return buildSdJwtCredentialConfig(name, template as SdJwtTemplate); + case CredentialFormat.JwtVcJsonLd: + return buildJwtVcJsonLdCredentialConfig(name, template as SdJwtTemplate); case CredentialFormat.Mdoc: return buildMdocCredentialConfig(name, template as MdocTemplate); default: @@ -361,7 +392,11 @@ export function buildCredentialConfigurationsSupported(templateRows: any): Recor const credentialConfig = buildCredentialConfig( templateRow.name, templateToBuild, - format === CredentialFormat.Mdoc ? CredentialFormat.Mdoc : CredentialFormat.SdJwtVc + format === CredentialFormat.Mdoc + ? CredentialFormat.Mdoc + : format === CredentialFormat.JwtVcJsonLd + ? CredentialFormat.JwtVcJsonLd + : CredentialFormat.SdJwtVc ); const appearanceJson = coerceJsonObject(templateRow.appearance); diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index e35b8c1bd..e0ec32df2 100755 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -372,7 +372,8 @@ export enum X509ExtendedKeyUsage { export enum CredentialFormat { SdJwtVc = 'dc+sd-jwt', - Mdoc = 'mso_mdoc' + Mdoc = 'mso_mdoc', + JwtVcJsonLd = 'jwt_vc_json-ld' } export enum AttributeType { From 401175791fb5f11424c309ed44144bf810c2635e Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Wed, 20 May 2026 15:46:31 +0530 Subject: [PATCH 02/15] feat(oid4vc): add support for jwt_vc_json-ld credential format Signed-off-by: Sagar Khole --- apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index 1aafb7d09..ce1f145fc 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -266,7 +266,8 @@ function buildClaimsFromTemplate(template: SdJwtTemplate | MdocTemplate): Claim[ return claims; } -export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate): Record { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate) { const formatSuffix = 'sdjwt'; // Determine the unique key for this credential configuration @@ -295,7 +296,8 @@ export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate }; } -export function buildMdocCredentialConfig(name: string, template: MdocTemplate): Record { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildMdocCredentialConfig(name: string, template: MdocTemplate) { //const claims: Claim[] = []; const formatSuffix = 'mdoc'; @@ -329,10 +331,8 @@ export function buildMdocCredentialConfig(name: string, template: MdocTemplate): }; } -export function buildJwtVcJsonLdCredentialConfig( - name: string, - template: SdJwtTemplate -): Record { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildJwtVcJsonLdCredentialConfig(name: string, template: SdJwtTemplate) { const formatSuffix = 'jwt-vc-json-ld'; // Determine the unique key for this credential configuration From 03152e46be2ed54c46373b1be85c70ba8aa83c11 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Thu, 21 May 2026 06:07:48 +0530 Subject: [PATCH 03/15] refactor(oid4vc): fix DTO formatting and nbf/exp verification Signed-off-by: Sagar Khole --- .../libs/helpers/credential-sessions.builder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 6b187ba07..08ffdc116 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -541,10 +541,10 @@ function buildJwtVcJsonLdCredential( credentialSubject: payloadCopy }; - if (nbf) { + if (nbf !== undefined) { wrappedPayload.nbf = nbf; } - if (exp) { + if (exp !== undefined) { wrappedPayload.exp = exp; } From e9319361d9bf3346afe13329d54b5088f551240d Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Fri, 5 Jun 2026 08:14:33 +0530 Subject: [PATCH 04/15] feat(oid4vc): fix jwt_vc_json-ld support and add holder service Signed-off-by: Sagar Khole --- apps/api-gateway/src/app.module.ts | 3 + .../oid4vc-holder/dtos/oid4vc-holder.dto.ts | 40 ++++ .../oid4vc-holder/oid4vc-holder.controller.ts | 128 ++++++++++ .../src/oid4vc-holder/oid4vc-holder.module.ts | 28 +++ .../oid4vc-holder/oid4vc-holder.service.ts | 40 ++++ .../dtos/oid4vc-issuer-template.dto.ts | 8 + .../oid4vc-issuance/oid4vc-issuance.module.ts | 2 +- .../dtos/oid4vc-verifier-presentation.dto.ts | 12 +- .../src/interfaces/oid4vc-holder.interface.ts | 17 ++ apps/oid4vc-holder/src/main.ts | 27 +++ .../src/oid4vc-holder.controller.ts | 46 ++++ .../oid4vc-holder/src/oid4vc-holder.module.ts | 38 +++ .../src/oid4vc-holder.service.ts | 102 ++++++++ apps/oid4vc-issuance/constant/issuance.ts | 3 + .../interfaces/oid4vc-template.interfaces.ts | 1 + .../helpers/credential-sessions.builder.ts | 24 ++ .../libs/helpers/issuer.metadata.ts | 225 ++++++++---------- .../src/oid4vc-issuance.service.ts | 87 ++++++- libs/common/src/common.constant.ts | 13 + libs/common/src/common.utils.ts | 21 +- 20 files changed, 726 insertions(+), 139 deletions(-) create mode 100644 apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts create mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts create mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts create mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts create mode 100644 apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts create mode 100644 apps/oid4vc-holder/src/main.ts create mode 100644 apps/oid4vc-holder/src/oid4vc-holder.controller.ts create mode 100644 apps/oid4vc-holder/src/oid4vc-holder.module.ts create mode 100644 apps/oid4vc-holder/src/oid4vc-holder.service.ts diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 42076ad14..6d94a7cda 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -24,7 +24,9 @@ import { IssuanceModule } from './issuance/issuance.module'; import { LoggerModule } from '@credebl/logger/logger.module'; import { NotificationModule } from './notification/notification.module'; import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; +import { Oid4vcHolderModule } from './oid4vc-holder/oid4vc-holder.module'; import { Oid4vpModule } from './oid4vc-verification/oid4vc-verification.module'; + import { OrganizationModule } from './organization/organization.module'; import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { PlatformModule } from './platform/platform.module'; @@ -77,6 +79,7 @@ import { shouldLoadOidcModules } from '@credebl/common/common.utils'; GeoLocationModule, CloudWalletModule, ConditionalModule.registerWhen(Oid4vcIssuanceModule, shouldLoadOidcModules), + ConditionalModule.registerWhen(Oid4vcHolderModule, shouldLoadOidcModules), ConditionalModule.registerWhen(Oid4vpModule, shouldLoadOidcModules), ConditionalModule.registerWhen(X509Module, shouldLoadOidcModules), KeycloakConfigModule diff --git a/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts b/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts new file mode 100644 index 000000000..8ca580a57 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class OidcResolveCredentialOfferDto { + @ApiProperty({ example: 'openid-credential-offer://?credential_offer_uri=...' }) + @IsString() + @IsNotEmpty() + credentialOfferUri: string; +} + +export class OidcRequestCredentialDto { + @ApiProperty({ example: 'openid-credential-offer://?credential_offer_uri=...' }) + @IsString() + @IsNotEmpty() + credentialOfferUri: string; + + @ApiProperty({ example: ['UniversityDegree'] }) + @IsArray() + @IsNotEmpty() + credentialsToRequest: string[]; + + @ApiPropertyOptional({ example: '1234' }) + @IsString() + @IsOptional() + txCode?: string; +} + +export class OidcResolveProofRequestDto { + @ApiProperty({ example: 'openid-vc-request://?request_uri=...' }) + @IsString() + @IsNotEmpty() + proofRequestUri: string; +} + +export class OidcAcceptProofRequestDto { + @ApiProperty({ example: 'openid-vc-request://?request_uri=...' }) + @IsString() + @IsNotEmpty() + proofRequestUri: string; +} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts new file mode 100644 index 000000000..1c22355b4 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts @@ -0,0 +1,128 @@ +import { Controller, Post, Body, UseGuards, HttpStatus, Res, Param, UseFilters, Logger } from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiForbiddenResponse, + ApiUnauthorizedResponse +} from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { Response } from 'express'; +import { IResponse } from '@credebl/common/interfaces/response.interface'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +import { Oid4vcHolderService } from './oid4vc-holder.service'; +import { + OidcAcceptProofRequestDto, + OidcRequestCredentialDto, + OidcResolveCredentialOfferDto, + OidcResolveProofRequestDto +} from './dtos/oid4vc-holder.dto'; + +@Controller('orgs/:orgId/oid4vc/holder') +@UseFilters(CustomExceptionFilter) +@ApiTags('OID4VC-Holder') +@ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) +export class Oid4vcHolderController { + private readonly logger = new Logger('Oid4vcHolderController'); + constructor(private readonly oid4vcHolderService: Oid4vcHolderService) {} + + @Post('resolve-credential-offer') + @ApiOperation({ + summary: 'Resolve OID4VC Credential Offer', + description: 'Resolves an OID4VC credential offer for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credential offer resolved successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcHolderResolveCredentialOffer( + @Param('orgId') orgId: string, + @Body() resolveDto: OidcResolveCredentialOfferDto, + @Res() res: Response + ): Promise { + const data = await this.oid4vcHolderService.oidcHolderResolveCredentialOffer(orgId, resolveDto); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: 'Credential offer resolved successfully.', + data + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('request-credential') + @ApiOperation({ + summary: 'Request OID4VC Credential', + description: 'Requests and stores an OID4VC credential for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credential requested successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcHolderRequestCredential( + @Param('orgId') orgId: string, + @Body() requestDto: OidcRequestCredentialDto, + @Res() res: Response + ): Promise { + const data = await this.oid4vcHolderService.oidcHolderRequestCredential(orgId, requestDto); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: 'Credential requested successfully.', + data + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('resolve-proof-request') + @ApiOperation({ + summary: 'Resolve OID4VC Proof Request', + description: 'Resolves an OID4VC proof request for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Proof request resolved successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcHolderResolveProofRequest( + @Param('orgId') orgId: string, + @Body() resolveDto: OidcResolveProofRequestDto, + @Res() res: Response + ): Promise { + const data = await this.oid4vcHolderService.oidcHolderResolveProofRequest(orgId, resolveDto); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: 'Proof request resolved successfully.', + data + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('accept-proof-request') + @ApiOperation({ + summary: 'Accept OID4VC Proof Request', + description: 'Accepts an OID4VC proof request for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Proof request accepted successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcHolderAcceptProofRequest( + @Param('orgId') orgId: string, + @Body() acceptDto: OidcAcceptProofRequestDto, + @Res() res: Response + ): Promise { + const data = await this.oid4vcHolderService.oidcHolderAcceptProofRequest(orgId, acceptDto); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: 'Proof request accepted successfully.', + data + }; + return res.status(HttpStatus.OK).json(finalResponse); + } +} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts new file mode 100644 index 000000000..5cb87b1ee --- /dev/null +++ b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts @@ -0,0 +1,28 @@ +import { Module } from '@nestjs/common'; +import { NATSClient } from '@credebl/common/NATSClient'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { HttpModule } from '@nestjs/axios'; +import { Oid4vcHolderController } from './oid4vc-holder.controller'; +import { Oid4vcHolderService } from './oid4vc-holder.service'; + +@Module({ + imports: [ + HttpModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions( + CommonConstants.OIDC4VC_HOLDER_SERVICE, + process.env.API_GATEWAY_NKEY_SEED, + process.env.NATS_CREDS_FILE + ) + } + ]) + ], + controllers: [Oid4vcHolderController], + providers: [Oid4vcHolderService, NATSClient] +}) +export class Oid4vcHolderModule {} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts new file mode 100644 index 000000000..bdd976488 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts @@ -0,0 +1,40 @@ +import { NATSClient } from '@credebl/common/NATSClient'; +import { Inject, Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +import { + OidcAcceptProofRequestDto, + OidcRequestCredentialDto, + OidcResolveCredentialOfferDto, + OidcResolveProofRequestDto +} from './dtos/oid4vc-holder.dto'; + +@Injectable() +export class Oid4vcHolderService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly holderProxy: ClientProxy, + private readonly natsClient: NATSClient + ) { + super('Oid4vcHolderService'); + } + + async oidcHolderResolveCredentialOffer(orgId: string, holderPayload: OidcResolveCredentialOfferDto): Promise { + const payload = { orgId, holderPayload }; + return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-resolve-credential-offer', payload); + } + + async oidcHolderRequestCredential(orgId: string, holderPayload: OidcRequestCredentialDto): Promise { + const payload = { orgId, holderPayload }; + return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-request-credential', payload); + } + + async oidcHolderResolveProofRequest(orgId: string, holderPayload: OidcResolveProofRequestDto): Promise { + const payload = { orgId, holderPayload }; + return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-resolve-proof-request', payload); + } + + async oidcHolderAcceptProofRequest(orgId: string, holderPayload: OidcAcceptProofRequestDto): Promise { + const payload = { orgId, holderPayload }; + return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-accept-proof-request', payload); + } +} diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts index b8ee8e5ca..7649c002c 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts @@ -196,6 +196,14 @@ export class SdJwtTemplateDto { @IsString() vct: string; + @ApiPropertyOptional({ + example: ['https://www.w3.org/2018/credentials/v1', 'https://json-ld.org/contexts/person.jsonld'], + description: 'JSON-LD context for the credential (optional, defaults to person schema for JSON-LD format)' + }) + @IsArray() + @IsOptional() + context?: string[]; + @ApiProperty({ type: 'array', items: { $ref: getSchemaPath(CredentialAttributeDto) }, diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts index f6833f4e8..ec5e70f91 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts @@ -15,7 +15,7 @@ import { HttpModule } from '@nestjs/axios'; name: 'NATS_CLIENT', transport: Transport.NATS, options: getNatsOptions( - CommonConstants.ISSUANCE_SERVICE, + CommonConstants.OIDC4VC_ISSUANCE_SERVICE, process.env.API_GATEWAY_NKEY_SEED, process.env.NATS_CREDS_FILE ) diff --git a/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts b/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts index c26e004ec..9c85706ca 100644 --- a/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts +++ b/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts @@ -289,7 +289,7 @@ export class DcqlDto { @ValidatorConstraint({ name: 'OnlyOneOf', async: false }) export class OnlyOneOfConstraint implements ValidatorConstraintInterface { // eslint-disable-next-line @typescript-eslint/no-explicit-any - validate(_: any, args: ValidationArguments): Promise | boolean { + validate(_: unknown, args: ValidationArguments): Promise | boolean { // eslint-disable-next-line @typescript-eslint/no-explicit-any const object = args.object as Record; const properties = args.constraints as string[]; @@ -356,10 +356,18 @@ export class PresentationRequestDto { @IsDefined() @IsEnum(ResponseMode) responseMode: ResponseMode; - //TODO: check e2e flow and add ResponseMode based restrictions @IsOptional() expectedOrigins: string[]; + @ApiPropertyOptional({ + description: 'OpenID4VP version to use', + enum: ['v1', 'v1.draft21', 'v1.draft24'], + example: 'v1.draft24' + }) + @IsOptional() + @IsEnum(['v1', 'v1.draft21', 'v1.draft24']) + version?: 'v1' | 'v1.draft21' | 'v1.draft24'; + /** * Dummy property used to run a class-level validation ensuring mutual exclusivity. * This property is not serialized into requests/responses but is required so `class-validator` diff --git a/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts b/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts new file mode 100644 index 000000000..fc2f0e48b --- /dev/null +++ b/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts @@ -0,0 +1,17 @@ +export interface IOidcHolderResolveCredentialOffer { + credentialOfferUri: string; +} + +export interface IOidcHolderRequestCredential { + credentialOfferUri: string; + credentialsToRequest: string[]; + txCode?: string; +} + +export interface IOidcHolderResolveProofRequest { + proofRequestUri: string; +} + +export interface IOidcHolderAcceptProofRequest { + proofRequestUri: string; +} diff --git a/apps/oid4vc-holder/src/main.ts b/apps/oid4vc-holder/src/main.ts new file mode 100644 index 000000000..5e9310dd1 --- /dev/null +++ b/apps/oid4vc-holder/src/main.ts @@ -0,0 +1,27 @@ +import { NestFactory } from '@nestjs/core'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { Oid4vcHolderModule } from './oid4vc-holder.module'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice(Oid4vcHolderModule, { + transport: Transport.NATS, + options: getNatsOptions( + CommonConstants.OIDC4VC_HOLDER_SERVICE, + process.env.OIDC4VC_HOLDER_NKEY_SEED, + process.env.NATS_CREDS_FILE + ) + }); + app.useLogger(app.get(NestjsLoggerServiceAdapter)); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('OID4VC-Holder-Service Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/oid4vc-holder/src/oid4vc-holder.controller.ts b/apps/oid4vc-holder/src/oid4vc-holder.controller.ts new file mode 100644 index 000000000..802a20133 --- /dev/null +++ b/apps/oid4vc-holder/src/oid4vc-holder.controller.ts @@ -0,0 +1,46 @@ +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { Oid4vcHolderService } from './oid4vc-holder.service'; +import { + IOidcHolderAcceptProofRequest, + IOidcHolderRequestCredential, + IOidcHolderResolveCredentialOffer, + IOidcHolderResolveProofRequest +} from './interfaces/oid4vc-holder.interface'; + +@Controller() +export class Oid4vcHolderController { + constructor(private readonly oid4vcHolderService: Oid4vcHolderService) {} + + @MessagePattern({ cmd: 'oid4vc-holder-resolve-credential-offer' }) + async oidcHolderResolveCredentialOffer(payload: { + orgId: string; + holderPayload: IOidcHolderResolveCredentialOffer; + }): Promise { + return this.oid4vcHolderService.oidcHolderResolveCredentialOffer(payload.orgId, payload.holderPayload); + } + + @MessagePattern({ cmd: 'oid4vc-holder-request-credential' }) + async oidcHolderRequestCredential(payload: { + orgId: string; + holderPayload: IOidcHolderRequestCredential; + }): Promise { + return this.oid4vcHolderService.oidcHolderRequestCredential(payload.orgId, payload.holderPayload); + } + + @MessagePattern({ cmd: 'oid4vc-holder-resolve-proof-request' }) + async oidcHolderResolveProofRequest(payload: { + orgId: string; + holderPayload: IOidcHolderResolveProofRequest; + }): Promise { + return this.oid4vcHolderService.oidcHolderResolveProofRequest(payload.orgId, payload.holderPayload); + } + + @MessagePattern({ cmd: 'oid4vc-holder-accept-proof-request' }) + async oidcHolderAcceptProofRequest(payload: { + orgId: string; + holderPayload: IOidcHolderAcceptProofRequest; + }): Promise { + return this.oid4vcHolderService.oidcHolderAcceptProofRequest(payload.orgId, payload.holderPayload); + } +} diff --git a/apps/oid4vc-holder/src/oid4vc-holder.module.ts b/apps/oid4vc-holder/src/oid4vc-holder.module.ts new file mode 100644 index 000000000..0d89112b7 --- /dev/null +++ b/apps/oid4vc-holder/src/oid4vc-holder.module.ts @@ -0,0 +1,38 @@ +import { Module, Logger } from '@nestjs/common'; +import { Oid4vcHolderController } from './oid4vc-holder.controller'; +import { Oid4vcHolderService } from './oid4vc-holder.service'; +import { CommonModule } from '@credebl/common'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { LoggerModule } from '@credebl/logger'; +import { PrismaServiceModule } from '@credebl/prisma-service'; +import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; +import { GlobalConfigModule } from '@credebl/config'; +import { ContextInterceptorModule } from '@credebl/context'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { NATSClient } from '@credebl/common/NATSClient'; + +@Module({ + imports: [ + CommonModule, + LoggerModule, + PrismaServiceModule, + PlatformConfig, + GlobalConfigModule, + ContextInterceptorModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions( + CommonConstants.OIDC4VC_HOLDER_SERVICE, + process.env.OIDC4VC_HOLDER_NKEY_SEED, + process.env.NATS_CREDS_FILE + ) + } + ]) + ], + controllers: [Oid4vcHolderController], + providers: [Oid4vcHolderService, NATSClient, Logger] +}) +export class Oid4vcHolderModule {} diff --git a/apps/oid4vc-holder/src/oid4vc-holder.service.ts b/apps/oid4vc-holder/src/oid4vc-holder.service.ts new file mode 100644 index 000000000..c5c156df5 --- /dev/null +++ b/apps/oid4vc-holder/src/oid4vc-holder.service.ts @@ -0,0 +1,102 @@ +import { Injectable, Logger, Inject } from '@nestjs/common'; +import { CommonService } from '@credebl/common'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { PrismaService } from '@credebl/prisma-service'; +import { ClientProxy } from '@nestjs/microservices'; +import { NATSClient } from '@credebl/common/NATSClient'; +import { getAgentUrl } from '@credebl/common/common.utils'; +import { + IOidcHolderAcceptProofRequest, + IOidcHolderRequestCredential, + IOidcHolderResolveCredentialOffer, + IOidcHolderResolveProofRequest +} from './interfaces/oid4vc-holder.interface'; + +@Injectable() +export class Oid4vcHolderService { + private readonly logger = new Logger('Oid4vcHolderService'); + + constructor( + private readonly commonService: CommonService, + private readonly prisma: PrismaService, + @Inject('NATS_CLIENT') private readonly natsClientProxy: ClientProxy, + private readonly natsClient: NATSClient + ) {} + + private async getOrgAgentApiKey(orgId: string): Promise { + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { orgId } + }); + if (!agentDetails || !agentDetails.apiKey) { + throw new Error(`Agent API key not found for org: ${orgId}`); + } + const decryptedToken = await this.commonService.decryptPassword(agentDetails.apiKey); + return decryptedToken; + } + + async _getAgentEndpoint(orgId: string): Promise { + const payload = { orgId }; + const agentDetails = await this.natsClient.sendNatsMessage( + this.natsClientProxy, + 'get-agent-details-by-org-id', + payload + ); + return (agentDetails as { agentEndPoint: string }).agentEndPoint; + } + + async oidcHolderResolveCredentialOffer(orgId: string, payload: IOidcHolderResolveCredentialOffer): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_RESOLVE_OFFER); + const data = await this.commonService + .httpPost(url, payload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcHolderResolveCredentialOffer in holder service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcHolderRequestCredential(orgId: string, payload: IOidcHolderRequestCredential): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_REQUEST_CREDENTIAL); + const data = await this.commonService + .httpPost(url, payload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcHolderRequestCredential in holder service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcHolderResolveProofRequest(orgId: string, payload: IOidcHolderResolveProofRequest): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_RESOLVE_PROOF_REQUEST); + const data = await this.commonService + .httpPost(url, payload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcHolderResolveProofRequest in holder service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcHolderAcceptProofRequest(orgId: string, payload: IOidcHolderAcceptProofRequest): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_ACCEPT_PROOF_REQUEST); + const data = await this.commonService + .httpPost(url, payload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcHolderAcceptProofRequest in holder service : ${JSON.stringify(error)}`); + throw error; + } + } +} diff --git a/apps/oid4vc-issuance/constant/issuance.ts b/apps/oid4vc-issuance/constant/issuance.ts index c1adf5aa2..2865e3c6c 100644 --- a/apps/oid4vc-issuance/constant/issuance.ts +++ b/apps/oid4vc-issuance/constant/issuance.ts @@ -7,3 +7,6 @@ export const accessTokenSignerKeyType = { kty: 'OKP', crv: 'Ed25519' } as { crv: AccessTokenSignerKeyType; }; export const batchCredentialIssuanceDefault = 0; + +export const CREDENTIALS_CONTEXT_V1_URL = 'https://www.w3.org/2018/credentials/v1'; +export const CREDENTIALS_CONTEXT_V2_URL = 'https://www.w3.org/ns/credentials/v2'; diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts index e6a6ce61b..f1b235de2 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -2,6 +2,7 @@ import { Prisma, SignerOption } from '@prisma/client'; import { AttributeType, CredentialFormat } from '@credebl/enum/enum'; export interface SdJwtTemplate { vct: string; + context?: string[]; attributes: CredentialAttribute[]; } diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 08ffdc116..bf8d1b6a0 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -536,8 +536,25 @@ function buildJwtVcJsonLdCredential( const apiFormat = mapDbFormatToApiFormat(templateRecord.format); const idSuffix = formatSuffix(apiFormat); const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + const vct = (templateRecord.attributes as any)?.vct; + let typeName = ''; + if ('string' === typeof vct) { + const lastSlash = vct.lastIndexOf('/'); + typeName = -1 !== lastSlash ? vct.substring(lastSlash + 1) : vct; + } else { + typeName = templateRecord.name.replace(/\s+/g, ''); + } + + const templateContext = (templateRecord.attributes as any)?.context; + const context = Array.isArray(templateContext) ? templateContext : ['https://www.w3.org/2018/credentials/v1']; + + if (!context.includes('https://www.w3.org/2018/credentials/v1')) { + context.unshift('https://www.w3.org/2018/credentials/v1'); + } const wrappedPayload: Record = { + '@context': context, + type: ['VerifiableCredential', typeName], credentialSubject: payloadCopy }; @@ -548,6 +565,13 @@ function buildJwtVcJsonLdCredential( wrappedPayload.exp = exp; } + if (credentialRequest.validityInfo) { + wrappedPayload.issuanceDate = new Date(credentialRequest.validityInfo.validFrom).toISOString(); + wrappedPayload.expirationDate = new Date(credentialRequest.validityInfo.validUntil).toISOString(); + } else { + wrappedPayload.issuanceDate = new Date().toISOString(); + } + return { credentialSupportedId, signerOptions: templateSignerOption ? templateSignerOption : undefined, diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index ce1f145fc..f252330b5 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -3,7 +3,13 @@ import { oidc_issuer, Prisma } from '@prisma/client'; import { batchCredentialIssuanceDefault } from '../../constant/issuance'; import { CreateOidcCredentialOffer } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; import { IssuerResponse } from 'apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces'; -import { Claim, MdocTemplate, SdJwtTemplate } from 'apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces'; +import { + Claim, + ClaimDisplay, + CredentialAttribute, + MdocTemplate, + SdJwtTemplate +} from 'apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces'; import { CredentialFormat } from '@credebl/enum/enum'; type AttributeDisplay = { name: string; locale: string }; @@ -30,13 +36,6 @@ type Appearance = { display: CredentialDisplayItem[]; }; -// type Claim = { -// mandatory?: boolean; -// // value_type: string; -// path: string[]; -// display?: AttributeDisplay[]; -// }; - type CredentialConfig = { format: string; vct?: string; @@ -45,6 +44,8 @@ type CredentialConfig = { credential_signing_alg_values_supported: string[] | number[]; cryptographic_binding_methods_supported: string[]; + proof_types_supported?: Record; + credential_definition?: Record; credential_metadata: { claims: Claim[]; @@ -56,110 +57,57 @@ type CredentialConfigurationsSupported = { credentialConfigurationsSupported: Record; }; -// ---- Static Lists (as requested) ---- -const STATIC_CREDENTIAL_ALGS_FOR_SDJWT = ['ES256', 'EdDSA'] as const; -const STATIC_CREDENTIAL_ALGS_FOR_MDOC = [-7, -9] as const; -const STATIC_BINDING_METHODS_FOR_SDJWT = ['did:key', 'did:web', 'did:jwk', 'jwk'] as const; -const STATIC_BINDING_METHODS_FOR_MDOC = ['cose_key'] as const; // We need to test 'did:key', 'did:web', 'did:jwk', 'jwk', +const ISSUER_DPOP_ALGS_DEFAULT = ['RS256', 'ES256']; -// Safe coercion helpers -function coerceJsonObject(v: Prisma.JsonValue): T | null { - if (null == v) { - return null; - } - if ('string' === typeof v) { - try { - return JSON.parse(v) as T; - } catch { - return null; - } - } - return v as unknown as T; // already a JsonObject/JsonArray -} -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// function isAttributesMap(x: any): x is AttributesMap { -// return x && 'object' === typeof x && Array.isArray(x); -// } -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// function isAppearance(x: any): x is Appearance { -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// return x && 'object' === typeof x && Array.isArray((x as any).display); -// } - -// // Prisma row shape -// type TemplateRowPrisma = { -// id: string; -// name: string; -// description?: string | null; -// format?: string | null; -// canBeRevoked?: boolean | null; -// attributes: Prisma.JsonValue; // JsonValue from DB -// appearance: Prisma.JsonValue; // JsonValue from DB -// issuerId: string; -// createdAt?: Date | string; -// updatedAt?: Date | string; -// }; - -// Prisma row shape -//TODO: Fix this eslint issue -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type TemplateRowPrisma = { - id: string; - name: string; - description?: string | null; - format?: string | null; - canBeRevoked?: boolean | null; - attributes: SdJwtTemplate | MdocTemplate; // JsonValue from DB - appearance: Prisma.JsonValue; // JsonValue from DB - issuerId: string; - createdAt?: Date | string; - updatedAt?: Date | string; -}; +const STATIC_CREDENTIAL_ALGS_FOR_SDJWT = ['ES256']; +const STATIC_BINDING_METHODS_FOR_SDJWT = ['did:key', 'did:web', 'did:jwk', 'jwk']; -// Default DPoP list for issuer-level metadata (match your example) -const ISSUER_DPOP_ALGS_DEFAULT = ['RS256', 'ES256'] as const; +const STATIC_CREDENTIAL_ALGS_FOR_MDOC = ['ES256']; +const STATIC_BINDING_METHODS_FOR_MDOC = ['jwk']; -// ---------- Safe coercion ---------- -function coerceJson(v: Prisma.JsonValue): T | null { - if (null == v) { - return null; - } - if ('string' === typeof v) { - try { - return JSON.parse(v) as T; - } catch { - return null; - } - } - return v as unknown as T; +/** + * Checks if a value is an array of DisplayItem. + * (Simple runtime check, could be more elaborate) + */ +function isDisplayArray(val: unknown): val is DisplayItem[] { + return Array.isArray(val); } type DisplayItem = { - name: string; + name?: string; locale?: string; - description?: string; - logo?: { uri: string; alt_text?: string }; + logo?: { + uri: string; + alt_text?: string; + }; }; -function isDisplayArray(x: unknown): x is DisplayItem[] { - return ( - Array.isArray(x) && - x.every( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (i) => i && 'object' === typeof i && 'string' === typeof (i as any).name - ) - ); +/** + * Safely coerces a Prisma.JsonValue (from oidc_issuer.metadata) into a JS object. + */ +function coerceJson(val: Prisma.JsonValue): T { + if (null === val || undefined === val) { + return {} as T; + } + if ('string' === typeof val) { + try { + return JSON.parse(val) as T; + } catch { + return {} as T; + } + } + return val as T; } -// ---------- Builder you asked for ---------- /** - * Build issuer metadata payload from issuer row + credential configurations. + * Builder #1: Issuer Metadata Payload + * + * Builds the root OIDC Issuer metadata object (including "display", "dpop...", etc.). * - * @param credentialConfigurations Object with credentialConfigurationsSupported (from your template builder) - * @param oidcIssuer OID4VC issuer row (uses publicIssuerId and metadata -> display) + * @param credentialConfigurations The "credential_configurations_supported" block built by Builder #2. + * @param oidcIssuer The Prisma row for the OIDC Issuer. * @param opts Optional overrides: dpopAlgs[], accessTokenSignerKeyType */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function buildIssuerPayload( credentialConfigurations: CredentialConfigurationsSupported, oidcIssuer: oidc_issuer, @@ -167,7 +115,7 @@ export function buildIssuerPayload( dpopAlgs?: string[]; accessTokenSignerKeyType?: string; } -) { +): Record { if (!oidcIssuer?.publicIssuerId || 'string' !== typeof oidcIssuer.publicIssuerId) { throw new Error('Invalid issuer: missing publicIssuerId'); } @@ -175,14 +123,21 @@ export function buildIssuerPayload( const rawDisplay = coerceJson(oidcIssuer.metadata); const display: DisplayItem[] = isDisplayArray(rawDisplay) ? rawDisplay : []; - return { + const batchSize = oidcIssuer?.batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault; + + const payload: Record = { display, dpopSigningAlgValuesSupported: opts?.dpopAlgs ?? [...ISSUER_DPOP_ALGS_DEFAULT], - credentialConfigurationsSupported: credentialConfigurations.credentialConfigurationsSupported ?? {}, - batchCredentialIssuance: { - batchSize: oidcIssuer?.batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault - } + credentialConfigurationsSupported: credentialConfigurations.credentialConfigurationsSupported ?? {} }; + + if (0 < batchSize) { + payload.batchCredentialIssuance = { + batchSize + }; + } + + return payload; } export function extractTemplateIds(offer: CreateOidcCredentialOffer): string[] { @@ -211,7 +166,7 @@ export function encodeIssuerPublicId(publicIssuerId: string): string { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -function generateClaims(attributes: any[], namespace?: string, parentPath: string[] = []): Claim[] { +function generateClaims(attributes: CredentialAttribute[], namespace?: string, parentPath: string[] = []): Claim[] { const result: Claim[] = []; for (const attr of attributes) { @@ -222,7 +177,7 @@ function generateClaims(attributes: any[], namespace?: string, parentPath: strin const claim: Claim = { path }; if (attr.display) { - claim.display = attr.display; + claim.display = attr.display as ClaimDisplay[]; } if (true === attr.mandatory) { @@ -267,7 +222,7 @@ function buildClaimsFromTemplate(template: SdJwtTemplate | MdocTemplate): Claim[ } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate) { +export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate): Record { const formatSuffix = 'sdjwt'; // Determine the unique key for this credential configuration @@ -297,7 +252,7 @@ export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildMdocCredentialConfig(name: string, template: MdocTemplate) { +export function buildMdocCredentialConfig(name: string, template: MdocTemplate): Record { //const claims: Claim[] = []; const formatSuffix = 'mdoc'; @@ -332,7 +287,10 @@ export function buildMdocCredentialConfig(name: string, template: MdocTemplate) } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildJwtVcJsonLdCredentialConfig(name: string, template: SdJwtTemplate) { +export function buildJwtVcJsonLdCredentialConfig( + name: string, + template: SdJwtTemplate +): Record { const formatSuffix = 'jwt-vc-json-ld'; // Determine the unique key for this credential configuration @@ -341,6 +299,13 @@ export function buildJwtVcJsonLdCredentialConfig(name: string, template: SdJwtTe const claims = buildClaimsFromTemplate(template); + const { vct } = template; + let typeName = name ? name.replace(/[^a-zA-Z0-9]/g, '') : 'GenericCredential'; + if ('string' === typeof vct && '' !== vct.trim()) { + const lastSlash = vct.lastIndexOf('/'); + typeName = -1 !== lastSlash ? vct.substring(lastSlash + 1) : vct; + } + return { [configKey]: { format: CredentialFormat.JwtVcJsonLd, @@ -353,6 +318,10 @@ export function buildJwtVcJsonLdCredentialConfig(name: string, template: SdJwtTe proof_signing_alg_values_supported: ['ES256', 'EdDSA'] } }, + credential_definition: { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential', typeName] + }, credential_metadata: { claims, display: [] @@ -363,7 +332,11 @@ export function buildJwtVcJsonLdCredentialConfig(name: string, template: SdJwtTe //TODO: Fix this eslint issue // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildCredentialConfig(name: string, template: SdJwtTemplate | MdocTemplate, format: CredentialFormat) { +export function buildCredentialConfig( + name: string, + template: SdJwtTemplate | MdocTemplate, + format: CredentialFormat +): Record { switch (format) { case CredentialFormat.SdJwtVc: return buildSdJwtCredentialConfig(name, template as SdJwtTemplate); @@ -380,17 +353,15 @@ export function buildCredentialConfig(name: string, template: SdJwtTemplate | Md * Build agent payload from Prisma rows (attributes/appearance are Prisma.JsonValue). * Safely coerces JSON and then builds the same structure as Builder #2. */ -//TODO: Fix this eslint issue -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function buildCredentialConfigurationsSupported(templateRows: any): Record { +export function buildCredentialConfigurationsSupported(templateRows: unknown[]): Record { const credentialConfigMap: Record = {}; - for (const templateRow of templateRows) { - const { format } = templateRow; - const templateToBuild = templateRow.attributes; + for (const templateRow of templateRows as Record[]) { + const format = templateRow.format as string; + const templateToBuild = templateRow.attributes as SdJwtTemplate | MdocTemplate; const credentialConfig = buildCredentialConfig( - templateRow.name, + templateRow.name as string, templateToBuild, format === CredentialFormat.Mdoc ? CredentialFormat.Mdoc @@ -398,25 +369,15 @@ export function buildCredentialConfigurationsSupported(templateRows: any): Recor ? CredentialFormat.JwtVcJsonLd : CredentialFormat.SdJwtVc ); - const appearanceJson = coerceJsonObject(templateRow.appearance); - // Prepare the display configuration - const displayConfigurations = - (appearanceJson as Appearance).display?.map((displayEntry) => ({ + const appearance = coerceJson(templateRow.appearance as Prisma.JsonValue); + const displayConfigurations: CredentialDisplayItem[] = + appearance.display?.map((displayEntry) => ({ name: displayEntry.name, - description: displayEntry.description, locale: displayEntry.locale, - logo: displayEntry?.logo - ? { - uri: displayEntry.logo.uri, - alt_text: displayEntry.logo.alt_text - } - : undefined, - background_image: displayEntry?.background_image?.uri - ? { - uri: displayEntry.background_image.uri - } - : undefined, + logo: displayEntry.logo, + description: displayEntry.description, + background_image: displayEntry.background_image, background_color: displayEntry.background_color, text_color: displayEntry.text_color })) ?? []; diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 859373257..86111e550 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -40,8 +40,11 @@ import { accessTokenSignerKeyType, batchCredentialIssuanceDefault, credentialConfigurationsSupported, - dpopSigningAlgValuesSupported + dpopSigningAlgValuesSupported, + CREDENTIALS_CONTEXT_V1_URL, + CREDENTIALS_CONTEXT_V2_URL } from '../constant/issuance'; +import { uuidRegex } from '@credebl/common/common.constant'; import { buildCredentialConfigurationsSupported, buildIssuerPayload, @@ -86,6 +89,81 @@ export class Oid4vcIssuanceService { private readonly statusListAllocatorService: StatusListAllocatorService ) {} + private async validateTemplateAgainstSchema( + format: string | CredentialFormat, + template: any, + orgId: string + ): Promise { + if ( + format === CredentialFormat.JwtVcJsonLd && + template && + 'context' in template && + Array.isArray(template.context) + ) { + const contexts = template.context; + let internalSchemaId: string | undefined; + + for (const url of contexts) { + if (url === CREDENTIALS_CONTEXT_V1_URL || url === CREDENTIALS_CONTEXT_V2_URL) { + continue; + } + if (url.includes('/schemas/')) { + const parts = url.split('/'); + const potentialUuid = parts[parts.length - 1]; + if (uuidRegex.test(potentialUuid)) { + internalSchemaId = potentialUuid; + break; + } + } + } + + if (internalSchemaId) { + let schemaResponse; + try { + schemaResponse = await this.issuanceServiceProxy + .send({ cmd: 'get-schema-by-id' }, { schemaId: internalSchemaId, orgId }) + .toPromise(); + } catch (error) { + this.logger.error(`Error fetching schema ${internalSchemaId} from ledger: ${error.message}`); + return; + } + + if (schemaResponse && schemaResponse.attributes) { + const schemaAttributes = + 'string' === typeof schemaResponse.attributes + ? JSON.parse(schemaResponse.attributes) + : schemaResponse.attributes; + const schemaAttributeNames = schemaAttributes.map((attr: any) => attr.attributeName); + const templateAttributes = template.attributes; + + const missingAttributes: string[] = []; + const checkAttributes = (attrs: any[]) => { + if (!attrs) { + return; + } + for (const attr of attrs) { + if (!schemaAttributeNames.includes(attr.key)) { + missingAttributes.push(attr.key); + } + if (attr.children) { + checkAttributes(attr.children); + } + } + }; + checkAttributes(templateAttributes); + + if (0 < missingAttributes.length) { + throw new BadRequestException( + `Attributes [ '${missingAttributes.join("', '")}' ] are not defined in the referenced schema.` + ); + } + } + } else { + this.logger.log('No internal schema reference found; skipping strict attribute validation.'); + } + } + } + async oidcIssuerCreate(issuerCreation: IssuerCreation, orgId: string, userDetails: user): Promise { try { const { issuerId, batchCredentialIssuanceSize } = issuerCreation; @@ -322,6 +400,8 @@ export class Oid4vcIssuanceService { //TODO: add revert mechanism if agent call fails const { name, description, format, canBeRevoked, appearance, signerOption, noticeUrl } = credentialTemplate; + await this.validateTemplateAgainstSchema(format, credentialTemplate.template, orgId); + const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer(name, issuerId); if (0 < checkNameExist.length) { throw new ConflictException(ResponseMessages.oidcTemplate.error.templateNameAlreadyExist); @@ -406,6 +486,11 @@ export class Oid4vcIssuanceService { ...(issuerId ? { issuerId } : {}) }; const { name, description, format, canBeRevoked, appearance, signerOption, noticeUrl } = normalized; + + if (normalized.template && (format || template.format)) { + await this.validateTemplateAgainstSchema(format || template.format, normalized.template, orgId); + } + const attributes = instanceToPlain(normalized.template); const payload = { diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 99d23bb4d..12a4be7b7 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -137,6 +137,12 @@ export enum CommonConstants { URL_OID4VP_VERIFICATION_SESSION = '/openid4vc/verification-sessions/create-presentation-request', URL_OIDC_VERIFIER_SESSION_AUTH_RESPONSE_VERIFY = '/openid4vc/verification-sessions/verify-authorization-response', + // OID4VC Holder URLs + URL_OIDC_HOLDER_RESOLVE_OFFER = '/openid4vc/holder/resolve-credential-offer', + URL_OIDC_HOLDER_REQUEST_CREDENTIAL = '/openid4vc/holder/request-credential', + URL_OIDC_HOLDER_RESOLVE_PROOF_REQUEST = '/openid4vc/holder/resolve-proof-request', + URL_OIDC_HOLDER_ACCEPT_PROOF_REQUEST = '/openid4vc/holder/accept-proof-request', + //X509 agent API URLs URL_CREATE_X509_CERTIFICATE = '/x509', URL_IMPORT_X509_CERTIFICATE = '/x509/import', @@ -397,6 +403,7 @@ export enum CommonConstants { CLOUD_WALLET_SERVICE = 'cloud-wallet', OIDC4VC_ISSUANCE_SERVICE = 'oid4vc-issuance', OIDC4VC_VERIFICATION_SERVICE = 'oid4vc-verification', + OIDC4VC_HOLDER_SERVICE = 'oid4vc-holder', OID4VP_VERIFICATION_SESSION = 'oid4vp-verification-session', X509_SERVICE = 'x509-service', @@ -434,6 +441,12 @@ export enum CommonConstants { OIDC_ISSUER_SESSIONS = 'get-oid4vc-sessions', OIDC_DELETE_CREDENTIAL_OFFER = 'delete-oid4vc-credential-offer', + // OID4VC Holder + OIDC_HOLDER_RESOLVE_OFFER = 'resolve-credential-offer', + OIDC_HOLDER_REQUEST_CREDENTIAL = 'request-credential', + OIDC_HOLDER_RESOLVE_PROOF_REQUEST = 'resolve-proof-request', + OIDC_HOLDER_ACCEPT_PROOF_REQUEST = 'accept-proof-request', + // OID4VP OIDC_VERIFIER_CREATE = 'create-oid4vp-verifier', OIDC_VERIFIER_UPDATE = 'update-oid4vp-verifier', diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index 98f92aeac..708b21074 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -25,9 +25,11 @@ export function paginator(items: T[], current_page: number, items_per_page: n }; } -export function orderValues(key, order = 'asc') { - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types - return function innerSort(a, b) { +export function orderValues( + key: string, + order = 'asc' +): (a: Record, b: Record) => number { + return function innerSort(a: Record, b: Record): number { if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) { // property doesn't exist on either object return 0; @@ -107,6 +109,19 @@ export const getAgentUrl = (agentEndPoint: string, urlFlag: string, paramId?: st [String(CommonConstants.OIDC_ISSUER_SESSIONS_BY_ID), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET)], [String(CommonConstants.OIDC_ISSUER_SESSIONS), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], [String(CommonConstants.OIDC_DELETE_CREDENTIAL_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], + [String(CommonConstants.OIDC_HOLDER_RESOLVE_OFFER), String(CommonConstants.URL_OIDC_HOLDER_RESOLVE_OFFER)], + [ + String(CommonConstants.OIDC_HOLDER_REQUEST_CREDENTIAL), + String(CommonConstants.URL_OIDC_HOLDER_REQUEST_CREDENTIAL) + ], + [ + String(CommonConstants.OIDC_HOLDER_RESOLVE_PROOF_REQUEST), + String(CommonConstants.URL_OIDC_HOLDER_RESOLVE_PROOF_REQUEST) + ], + [ + String(CommonConstants.OIDC_HOLDER_ACCEPT_PROOF_REQUEST), + String(CommonConstants.URL_OIDC_HOLDER_ACCEPT_PROOF_REQUEST) + ], [String(CommonConstants.X509_CREATE_CERTIFICATE), String(CommonConstants.URL_CREATE_X509_CERTIFICATE)], [String(CommonConstants.X509_DECODE_CERTIFICATE), String(CommonConstants.URL_DECODE_X509_CERTIFICATE)], [String(CommonConstants.X509_IMPORT_CERTIFICATE), String(CommonConstants.URL_IMPORT_X509_CERTIFICATE)], From 05b4fbbb8d2561804f7757e3922fa7becc94d491 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Mon, 15 Jun 2026 08:44:42 +0530 Subject: [PATCH 05/15] feat: replace context array with schemaUrl in SdJwtTemplate Signed-off-by: Sagar Khole --- .../dtos/oid4vc-issuer-template.dto.ts | 8 ++-- .../interfaces/oid4vc-template.interfaces.ts | 1 + .../src/oid4vc-issuance.service.ts | 41 +++++++++++-------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts index 7649c002c..784830047 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts @@ -197,12 +197,12 @@ export class SdJwtTemplateDto { vct: string; @ApiPropertyOptional({ - example: ['https://www.w3.org/2018/credentials/v1', 'https://json-ld.org/contexts/person.jsonld'], - description: 'JSON-LD context for the credential (optional, defaults to person schema for JSON-LD format)' + example: 'https://dev-schema.sovio.id/schemas/a99d55b6-c663-4af8-bcd3-4fe6699df91a', + description: 'JSON-LD schema URL for the credential' }) - @IsArray() + @IsString() @IsOptional() - context?: string[]; + schemaUrl?: string; @ApiProperty({ type: 'array', diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts index f1b235de2..9b7d72de0 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -2,6 +2,7 @@ import { Prisma, SignerOption } from '@prisma/client'; import { AttributeType, CredentialFormat } from '@credebl/enum/enum'; export interface SdJwtTemplate { vct: string; + schemaUrl?: string; context?: string[]; attributes: CredentialAttribute[]; } diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 86111e550..d8d7d0caf 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -94,26 +94,14 @@ export class Oid4vcIssuanceService { template: any, orgId: string ): Promise { - if ( - format === CredentialFormat.JwtVcJsonLd && - template && - 'context' in template && - Array.isArray(template.context) - ) { - const contexts = template.context; + if (format === CredentialFormat.JwtVcJsonLd && template) { let internalSchemaId: string | undefined; - for (const url of contexts) { - if (url === CREDENTIALS_CONTEXT_V1_URL || url === CREDENTIALS_CONTEXT_V2_URL) { - continue; - } - if (url.includes('/schemas/')) { - const parts = url.split('/'); - const potentialUuid = parts[parts.length - 1]; - if (uuidRegex.test(potentialUuid)) { - internalSchemaId = potentialUuid; - break; - } + if ('schemaUrl' in template && template.schemaUrl) { + const parts = template.schemaUrl.split('/'); + const potentialUuid = parts[parts.length - 1]; + if (uuidRegex.test(potentialUuid)) { + internalSchemaId = potentialUuid; } } @@ -400,6 +388,14 @@ export class Oid4vcIssuanceService { //TODO: add revert mechanism if agent call fails const { name, description, format, canBeRevoked, appearance, signerOption, noticeUrl } = credentialTemplate; + if ( + format === CredentialFormat.JwtVcJsonLd && + 'schemaUrl' in credentialTemplate.template && + credentialTemplate.template.schemaUrl + ) { + credentialTemplate.template.context = [CREDENTIALS_CONTEXT_V1_URL, credentialTemplate.template.schemaUrl]; + } + await this.validateTemplateAgainstSchema(format, credentialTemplate.template, orgId); const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer(name, issuerId); @@ -487,6 +483,15 @@ export class Oid4vcIssuanceService { }; const { name, description, format, canBeRevoked, appearance, signerOption, noticeUrl } = normalized; + if ( + normalized.template && + (format || template.format) === CredentialFormat.JwtVcJsonLd && + 'schemaUrl' in normalized.template && + normalized.template.schemaUrl + ) { + normalized.template.context = [CREDENTIALS_CONTEXT_V1_URL, normalized.template.schemaUrl]; + } + if (normalized.template && (format || template.format)) { await this.validateTemplateAgainstSchema(format || template.format, normalized.template, orgId); } From 70f94460aa40c37da12a2a5e1396c7304ab449ee Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Tue, 23 Jun 2026 06:10:28 +0530 Subject: [PATCH 06/15] refactor(oid4vc): remove oid4vc-holder microservice and gateway integration Signed-off-by: Sagar Khole --- apps/api-gateway/src/app.module.ts | 2 - .../oid4vc-holder/dtos/oid4vc-holder.dto.ts | 40 ------ .../oid4vc-holder/oid4vc-holder.controller.ts | 128 ------------------ .../src/oid4vc-holder/oid4vc-holder.module.ts | 28 ---- .../oid4vc-holder/oid4vc-holder.service.ts | 40 ------ .../src/interfaces/oid4vc-holder.interface.ts | 17 --- apps/oid4vc-holder/src/main.ts | 27 ---- .../src/oid4vc-holder.controller.ts | 46 ------- .../oid4vc-holder/src/oid4vc-holder.module.ts | 38 ------ .../src/oid4vc-holder.service.ts | 102 -------------- libs/common/src/common.constant.ts | 13 -- libs/common/src/common.utils.ts | 13 -- 12 files changed, 494 deletions(-) delete mode 100644 apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts delete mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts delete mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts delete mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts delete mode 100644 apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts delete mode 100644 apps/oid4vc-holder/src/main.ts delete mode 100644 apps/oid4vc-holder/src/oid4vc-holder.controller.ts delete mode 100644 apps/oid4vc-holder/src/oid4vc-holder.module.ts delete mode 100644 apps/oid4vc-holder/src/oid4vc-holder.service.ts diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 6d94a7cda..3200a9219 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -24,7 +24,6 @@ import { IssuanceModule } from './issuance/issuance.module'; import { LoggerModule } from '@credebl/logger/logger.module'; import { NotificationModule } from './notification/notification.module'; import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; -import { Oid4vcHolderModule } from './oid4vc-holder/oid4vc-holder.module'; import { Oid4vpModule } from './oid4vc-verification/oid4vc-verification.module'; import { OrganizationModule } from './organization/organization.module'; @@ -79,7 +78,6 @@ import { shouldLoadOidcModules } from '@credebl/common/common.utils'; GeoLocationModule, CloudWalletModule, ConditionalModule.registerWhen(Oid4vcIssuanceModule, shouldLoadOidcModules), - ConditionalModule.registerWhen(Oid4vcHolderModule, shouldLoadOidcModules), ConditionalModule.registerWhen(Oid4vpModule, shouldLoadOidcModules), ConditionalModule.registerWhen(X509Module, shouldLoadOidcModules), KeycloakConfigModule diff --git a/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts b/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts deleted file mode 100644 index 8ca580a57..000000000 --- a/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; - -export class OidcResolveCredentialOfferDto { - @ApiProperty({ example: 'openid-credential-offer://?credential_offer_uri=...' }) - @IsString() - @IsNotEmpty() - credentialOfferUri: string; -} - -export class OidcRequestCredentialDto { - @ApiProperty({ example: 'openid-credential-offer://?credential_offer_uri=...' }) - @IsString() - @IsNotEmpty() - credentialOfferUri: string; - - @ApiProperty({ example: ['UniversityDegree'] }) - @IsArray() - @IsNotEmpty() - credentialsToRequest: string[]; - - @ApiPropertyOptional({ example: '1234' }) - @IsString() - @IsOptional() - txCode?: string; -} - -export class OidcResolveProofRequestDto { - @ApiProperty({ example: 'openid-vc-request://?request_uri=...' }) - @IsString() - @IsNotEmpty() - proofRequestUri: string; -} - -export class OidcAcceptProofRequestDto { - @ApiProperty({ example: 'openid-vc-request://?request_uri=...' }) - @IsString() - @IsNotEmpty() - proofRequestUri: string; -} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts deleted file mode 100644 index 1c22355b4..000000000 --- a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Controller, Post, Body, UseGuards, HttpStatus, Res, Param, UseFilters, Logger } from '@nestjs/common'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiBearerAuth, - ApiForbiddenResponse, - ApiUnauthorizedResponse -} from '@nestjs/swagger'; -import { AuthGuard } from '@nestjs/passport'; -import { ApiResponseDto } from '../dtos/apiResponse.dto'; -import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; -import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; -import { Response } from 'express'; -import { IResponse } from '@credebl/common/interfaces/response.interface'; -import { Roles } from '../authz/decorators/roles.decorator'; -import { OrgRoles } from 'libs/org-roles/enums'; -import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; -import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; -import { Oid4vcHolderService } from './oid4vc-holder.service'; -import { - OidcAcceptProofRequestDto, - OidcRequestCredentialDto, - OidcResolveCredentialOfferDto, - OidcResolveProofRequestDto -} from './dtos/oid4vc-holder.dto'; - -@Controller('orgs/:orgId/oid4vc/holder') -@UseFilters(CustomExceptionFilter) -@ApiTags('OID4VC-Holder') -@ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) -@ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) -export class Oid4vcHolderController { - private readonly logger = new Logger('Oid4vcHolderController'); - constructor(private readonly oid4vcHolderService: Oid4vcHolderService) {} - - @Post('resolve-credential-offer') - @ApiOperation({ - summary: 'Resolve OID4VC Credential Offer', - description: 'Resolves an OID4VC credential offer for the specified organization.' - }) - @ApiResponse({ status: HttpStatus.OK, description: 'Credential offer resolved successfully.', type: ApiResponseDto }) - @ApiBearerAuth() - @Roles(OrgRoles.OWNER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - async oidcHolderResolveCredentialOffer( - @Param('orgId') orgId: string, - @Body() resolveDto: OidcResolveCredentialOfferDto, - @Res() res: Response - ): Promise { - const data = await this.oid4vcHolderService.oidcHolderResolveCredentialOffer(orgId, resolveDto); - const finalResponse: IResponse = { - statusCode: HttpStatus.OK, - message: 'Credential offer resolved successfully.', - data - }; - return res.status(HttpStatus.OK).json(finalResponse); - } - - @Post('request-credential') - @ApiOperation({ - summary: 'Request OID4VC Credential', - description: 'Requests and stores an OID4VC credential for the specified organization.' - }) - @ApiResponse({ status: HttpStatus.OK, description: 'Credential requested successfully.', type: ApiResponseDto }) - @ApiBearerAuth() - @Roles(OrgRoles.OWNER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - async oidcHolderRequestCredential( - @Param('orgId') orgId: string, - @Body() requestDto: OidcRequestCredentialDto, - @Res() res: Response - ): Promise { - const data = await this.oid4vcHolderService.oidcHolderRequestCredential(orgId, requestDto); - const finalResponse: IResponse = { - statusCode: HttpStatus.OK, - message: 'Credential requested successfully.', - data - }; - return res.status(HttpStatus.OK).json(finalResponse); - } - - @Post('resolve-proof-request') - @ApiOperation({ - summary: 'Resolve OID4VC Proof Request', - description: 'Resolves an OID4VC proof request for the specified organization.' - }) - @ApiResponse({ status: HttpStatus.OK, description: 'Proof request resolved successfully.', type: ApiResponseDto }) - @ApiBearerAuth() - @Roles(OrgRoles.OWNER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - async oidcHolderResolveProofRequest( - @Param('orgId') orgId: string, - @Body() resolveDto: OidcResolveProofRequestDto, - @Res() res: Response - ): Promise { - const data = await this.oid4vcHolderService.oidcHolderResolveProofRequest(orgId, resolveDto); - const finalResponse: IResponse = { - statusCode: HttpStatus.OK, - message: 'Proof request resolved successfully.', - data - }; - return res.status(HttpStatus.OK).json(finalResponse); - } - - @Post('accept-proof-request') - @ApiOperation({ - summary: 'Accept OID4VC Proof Request', - description: 'Accepts an OID4VC proof request for the specified organization.' - }) - @ApiResponse({ status: HttpStatus.OK, description: 'Proof request accepted successfully.', type: ApiResponseDto }) - @ApiBearerAuth() - @Roles(OrgRoles.OWNER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - async oidcHolderAcceptProofRequest( - @Param('orgId') orgId: string, - @Body() acceptDto: OidcAcceptProofRequestDto, - @Res() res: Response - ): Promise { - const data = await this.oid4vcHolderService.oidcHolderAcceptProofRequest(orgId, acceptDto); - const finalResponse: IResponse = { - statusCode: HttpStatus.OK, - message: 'Proof request accepted successfully.', - data - }; - return res.status(HttpStatus.OK).json(finalResponse); - } -} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts deleted file mode 100644 index 5cb87b1ee..000000000 --- a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Module } from '@nestjs/common'; -import { NATSClient } from '@credebl/common/NATSClient'; -import { getNatsOptions } from '@credebl/common/nats.config'; -import { CommonConstants } from '@credebl/common/common.constant'; -import { ClientsModule, Transport } from '@nestjs/microservices'; -import { HttpModule } from '@nestjs/axios'; -import { Oid4vcHolderController } from './oid4vc-holder.controller'; -import { Oid4vcHolderService } from './oid4vc-holder.service'; - -@Module({ - imports: [ - HttpModule, - ClientsModule.register([ - { - name: 'NATS_CLIENT', - transport: Transport.NATS, - options: getNatsOptions( - CommonConstants.OIDC4VC_HOLDER_SERVICE, - process.env.API_GATEWAY_NKEY_SEED, - process.env.NATS_CREDS_FILE - ) - } - ]) - ], - controllers: [Oid4vcHolderController], - providers: [Oid4vcHolderService, NATSClient] -}) -export class Oid4vcHolderModule {} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts deleted file mode 100644 index bdd976488..000000000 --- a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NATSClient } from '@credebl/common/NATSClient'; -import { Inject, Injectable } from '@nestjs/common'; -import { ClientProxy } from '@nestjs/microservices'; -import { BaseService } from 'libs/service/base.service'; -import { - OidcAcceptProofRequestDto, - OidcRequestCredentialDto, - OidcResolveCredentialOfferDto, - OidcResolveProofRequestDto -} from './dtos/oid4vc-holder.dto'; - -@Injectable() -export class Oid4vcHolderService extends BaseService { - constructor( - @Inject('NATS_CLIENT') private readonly holderProxy: ClientProxy, - private readonly natsClient: NATSClient - ) { - super('Oid4vcHolderService'); - } - - async oidcHolderResolveCredentialOffer(orgId: string, holderPayload: OidcResolveCredentialOfferDto): Promise { - const payload = { orgId, holderPayload }; - return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-resolve-credential-offer', payload); - } - - async oidcHolderRequestCredential(orgId: string, holderPayload: OidcRequestCredentialDto): Promise { - const payload = { orgId, holderPayload }; - return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-request-credential', payload); - } - - async oidcHolderResolveProofRequest(orgId: string, holderPayload: OidcResolveProofRequestDto): Promise { - const payload = { orgId, holderPayload }; - return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-resolve-proof-request', payload); - } - - async oidcHolderAcceptProofRequest(orgId: string, holderPayload: OidcAcceptProofRequestDto): Promise { - const payload = { orgId, holderPayload }; - return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-accept-proof-request', payload); - } -} diff --git a/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts b/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts deleted file mode 100644 index fc2f0e48b..000000000 --- a/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface IOidcHolderResolveCredentialOffer { - credentialOfferUri: string; -} - -export interface IOidcHolderRequestCredential { - credentialOfferUri: string; - credentialsToRequest: string[]; - txCode?: string; -} - -export interface IOidcHolderResolveProofRequest { - proofRequestUri: string; -} - -export interface IOidcHolderAcceptProofRequest { - proofRequestUri: string; -} diff --git a/apps/oid4vc-holder/src/main.ts b/apps/oid4vc-holder/src/main.ts deleted file mode 100644 index 5e9310dd1..000000000 --- a/apps/oid4vc-holder/src/main.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { HttpExceptionFilter } from 'libs/http-exception.filter'; -import { Logger } from '@nestjs/common'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; -import { CommonConstants } from '@credebl/common/common.constant'; -import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { Oid4vcHolderModule } from './oid4vc-holder.module'; - -const logger = new Logger(); - -async function bootstrap(): Promise { - const app = await NestFactory.createMicroservice(Oid4vcHolderModule, { - transport: Transport.NATS, - options: getNatsOptions( - CommonConstants.OIDC4VC_HOLDER_SERVICE, - process.env.OIDC4VC_HOLDER_NKEY_SEED, - process.env.NATS_CREDS_FILE - ) - }); - app.useLogger(app.get(NestjsLoggerServiceAdapter)); - app.useGlobalFilters(new HttpExceptionFilter()); - - await app.listen(); - logger.log('OID4VC-Holder-Service Microservice is listening to NATS '); -} -bootstrap(); diff --git a/apps/oid4vc-holder/src/oid4vc-holder.controller.ts b/apps/oid4vc-holder/src/oid4vc-holder.controller.ts deleted file mode 100644 index 802a20133..000000000 --- a/apps/oid4vc-holder/src/oid4vc-holder.controller.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Controller } from '@nestjs/common'; -import { MessagePattern } from '@nestjs/microservices'; -import { Oid4vcHolderService } from './oid4vc-holder.service'; -import { - IOidcHolderAcceptProofRequest, - IOidcHolderRequestCredential, - IOidcHolderResolveCredentialOffer, - IOidcHolderResolveProofRequest -} from './interfaces/oid4vc-holder.interface'; - -@Controller() -export class Oid4vcHolderController { - constructor(private readonly oid4vcHolderService: Oid4vcHolderService) {} - - @MessagePattern({ cmd: 'oid4vc-holder-resolve-credential-offer' }) - async oidcHolderResolveCredentialOffer(payload: { - orgId: string; - holderPayload: IOidcHolderResolveCredentialOffer; - }): Promise { - return this.oid4vcHolderService.oidcHolderResolveCredentialOffer(payload.orgId, payload.holderPayload); - } - - @MessagePattern({ cmd: 'oid4vc-holder-request-credential' }) - async oidcHolderRequestCredential(payload: { - orgId: string; - holderPayload: IOidcHolderRequestCredential; - }): Promise { - return this.oid4vcHolderService.oidcHolderRequestCredential(payload.orgId, payload.holderPayload); - } - - @MessagePattern({ cmd: 'oid4vc-holder-resolve-proof-request' }) - async oidcHolderResolveProofRequest(payload: { - orgId: string; - holderPayload: IOidcHolderResolveProofRequest; - }): Promise { - return this.oid4vcHolderService.oidcHolderResolveProofRequest(payload.orgId, payload.holderPayload); - } - - @MessagePattern({ cmd: 'oid4vc-holder-accept-proof-request' }) - async oidcHolderAcceptProofRequest(payload: { - orgId: string; - holderPayload: IOidcHolderAcceptProofRequest; - }): Promise { - return this.oid4vcHolderService.oidcHolderAcceptProofRequest(payload.orgId, payload.holderPayload); - } -} diff --git a/apps/oid4vc-holder/src/oid4vc-holder.module.ts b/apps/oid4vc-holder/src/oid4vc-holder.module.ts deleted file mode 100644 index 0d89112b7..000000000 --- a/apps/oid4vc-holder/src/oid4vc-holder.module.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Module, Logger } from '@nestjs/common'; -import { Oid4vcHolderController } from './oid4vc-holder.controller'; -import { Oid4vcHolderService } from './oid4vc-holder.service'; -import { CommonModule } from '@credebl/common'; -import { CommonConstants } from '@credebl/common/common.constant'; -import { LoggerModule } from '@credebl/logger'; -import { PrismaServiceModule } from '@credebl/prisma-service'; -import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; -import { GlobalConfigModule } from '@credebl/config'; -import { ContextInterceptorModule } from '@credebl/context'; -import { ClientsModule, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; -import { NATSClient } from '@credebl/common/NATSClient'; - -@Module({ - imports: [ - CommonModule, - LoggerModule, - PrismaServiceModule, - PlatformConfig, - GlobalConfigModule, - ContextInterceptorModule, - ClientsModule.register([ - { - name: 'NATS_CLIENT', - transport: Transport.NATS, - options: getNatsOptions( - CommonConstants.OIDC4VC_HOLDER_SERVICE, - process.env.OIDC4VC_HOLDER_NKEY_SEED, - process.env.NATS_CREDS_FILE - ) - } - ]) - ], - controllers: [Oid4vcHolderController], - providers: [Oid4vcHolderService, NATSClient, Logger] -}) -export class Oid4vcHolderModule {} diff --git a/apps/oid4vc-holder/src/oid4vc-holder.service.ts b/apps/oid4vc-holder/src/oid4vc-holder.service.ts deleted file mode 100644 index c5c156df5..000000000 --- a/apps/oid4vc-holder/src/oid4vc-holder.service.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Injectable, Logger, Inject } from '@nestjs/common'; -import { CommonService } from '@credebl/common'; -import { CommonConstants } from '@credebl/common/common.constant'; -import { PrismaService } from '@credebl/prisma-service'; -import { ClientProxy } from '@nestjs/microservices'; -import { NATSClient } from '@credebl/common/NATSClient'; -import { getAgentUrl } from '@credebl/common/common.utils'; -import { - IOidcHolderAcceptProofRequest, - IOidcHolderRequestCredential, - IOidcHolderResolveCredentialOffer, - IOidcHolderResolveProofRequest -} from './interfaces/oid4vc-holder.interface'; - -@Injectable() -export class Oid4vcHolderService { - private readonly logger = new Logger('Oid4vcHolderService'); - - constructor( - private readonly commonService: CommonService, - private readonly prisma: PrismaService, - @Inject('NATS_CLIENT') private readonly natsClientProxy: ClientProxy, - private readonly natsClient: NATSClient - ) {} - - private async getOrgAgentApiKey(orgId: string): Promise { - const agentDetails = await this.prisma.org_agents.findFirst({ - where: { orgId } - }); - if (!agentDetails || !agentDetails.apiKey) { - throw new Error(`Agent API key not found for org: ${orgId}`); - } - const decryptedToken = await this.commonService.decryptPassword(agentDetails.apiKey); - return decryptedToken; - } - - async _getAgentEndpoint(orgId: string): Promise { - const payload = { orgId }; - const agentDetails = await this.natsClient.sendNatsMessage( - this.natsClientProxy, - 'get-agent-details-by-org-id', - payload - ); - return (agentDetails as { agentEndPoint: string }).agentEndPoint; - } - - async oidcHolderResolveCredentialOffer(orgId: string, payload: IOidcHolderResolveCredentialOffer): Promise { - try { - const getApiKey = await this.getOrgAgentApiKey(orgId); - const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_RESOLVE_OFFER); - const data = await this.commonService - .httpPost(url, payload, { headers: { authorization: getApiKey } }) - .then(async (response) => response); - return data; - } catch (error) { - this.logger.error(`Error in oidcHolderResolveCredentialOffer in holder service : ${JSON.stringify(error)}`); - throw error; - } - } - - async oidcHolderRequestCredential(orgId: string, payload: IOidcHolderRequestCredential): Promise { - try { - const getApiKey = await this.getOrgAgentApiKey(orgId); - const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_REQUEST_CREDENTIAL); - const data = await this.commonService - .httpPost(url, payload, { headers: { authorization: getApiKey } }) - .then(async (response) => response); - return data; - } catch (error) { - this.logger.error(`Error in oidcHolderRequestCredential in holder service : ${JSON.stringify(error)}`); - throw error; - } - } - - async oidcHolderResolveProofRequest(orgId: string, payload: IOidcHolderResolveProofRequest): Promise { - try { - const getApiKey = await this.getOrgAgentApiKey(orgId); - const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_RESOLVE_PROOF_REQUEST); - const data = await this.commonService - .httpPost(url, payload, { headers: { authorization: getApiKey } }) - .then(async (response) => response); - return data; - } catch (error) { - this.logger.error(`Error in oidcHolderResolveProofRequest in holder service : ${JSON.stringify(error)}`); - throw error; - } - } - - async oidcHolderAcceptProofRequest(orgId: string, payload: IOidcHolderAcceptProofRequest): Promise { - try { - const getApiKey = await this.getOrgAgentApiKey(orgId); - const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_ACCEPT_PROOF_REQUEST); - const data = await this.commonService - .httpPost(url, payload, { headers: { authorization: getApiKey } }) - .then(async (response) => response); - return data; - } catch (error) { - this.logger.error(`Error in oidcHolderAcceptProofRequest in holder service : ${JSON.stringify(error)}`); - throw error; - } - } -} diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 12a4be7b7..99d23bb4d 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -137,12 +137,6 @@ export enum CommonConstants { URL_OID4VP_VERIFICATION_SESSION = '/openid4vc/verification-sessions/create-presentation-request', URL_OIDC_VERIFIER_SESSION_AUTH_RESPONSE_VERIFY = '/openid4vc/verification-sessions/verify-authorization-response', - // OID4VC Holder URLs - URL_OIDC_HOLDER_RESOLVE_OFFER = '/openid4vc/holder/resolve-credential-offer', - URL_OIDC_HOLDER_REQUEST_CREDENTIAL = '/openid4vc/holder/request-credential', - URL_OIDC_HOLDER_RESOLVE_PROOF_REQUEST = '/openid4vc/holder/resolve-proof-request', - URL_OIDC_HOLDER_ACCEPT_PROOF_REQUEST = '/openid4vc/holder/accept-proof-request', - //X509 agent API URLs URL_CREATE_X509_CERTIFICATE = '/x509', URL_IMPORT_X509_CERTIFICATE = '/x509/import', @@ -403,7 +397,6 @@ export enum CommonConstants { CLOUD_WALLET_SERVICE = 'cloud-wallet', OIDC4VC_ISSUANCE_SERVICE = 'oid4vc-issuance', OIDC4VC_VERIFICATION_SERVICE = 'oid4vc-verification', - OIDC4VC_HOLDER_SERVICE = 'oid4vc-holder', OID4VP_VERIFICATION_SESSION = 'oid4vp-verification-session', X509_SERVICE = 'x509-service', @@ -441,12 +434,6 @@ export enum CommonConstants { OIDC_ISSUER_SESSIONS = 'get-oid4vc-sessions', OIDC_DELETE_CREDENTIAL_OFFER = 'delete-oid4vc-credential-offer', - // OID4VC Holder - OIDC_HOLDER_RESOLVE_OFFER = 'resolve-credential-offer', - OIDC_HOLDER_REQUEST_CREDENTIAL = 'request-credential', - OIDC_HOLDER_RESOLVE_PROOF_REQUEST = 'resolve-proof-request', - OIDC_HOLDER_ACCEPT_PROOF_REQUEST = 'accept-proof-request', - // OID4VP OIDC_VERIFIER_CREATE = 'create-oid4vp-verifier', OIDC_VERIFIER_UPDATE = 'update-oid4vp-verifier', diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index 708b21074..03e646738 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -109,19 +109,6 @@ export const getAgentUrl = (agentEndPoint: string, urlFlag: string, paramId?: st [String(CommonConstants.OIDC_ISSUER_SESSIONS_BY_ID), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET)], [String(CommonConstants.OIDC_ISSUER_SESSIONS), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], [String(CommonConstants.OIDC_DELETE_CREDENTIAL_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], - [String(CommonConstants.OIDC_HOLDER_RESOLVE_OFFER), String(CommonConstants.URL_OIDC_HOLDER_RESOLVE_OFFER)], - [ - String(CommonConstants.OIDC_HOLDER_REQUEST_CREDENTIAL), - String(CommonConstants.URL_OIDC_HOLDER_REQUEST_CREDENTIAL) - ], - [ - String(CommonConstants.OIDC_HOLDER_RESOLVE_PROOF_REQUEST), - String(CommonConstants.URL_OIDC_HOLDER_RESOLVE_PROOF_REQUEST) - ], - [ - String(CommonConstants.OIDC_HOLDER_ACCEPT_PROOF_REQUEST), - String(CommonConstants.URL_OIDC_HOLDER_ACCEPT_PROOF_REQUEST) - ], [String(CommonConstants.X509_CREATE_CERTIFICATE), String(CommonConstants.URL_CREATE_X509_CERTIFICATE)], [String(CommonConstants.X509_DECODE_CERTIFICATE), String(CommonConstants.URL_DECODE_X509_CERTIFICATE)], [String(CommonConstants.X509_IMPORT_CERTIFICATE), String(CommonConstants.URL_IMPORT_X509_CERTIFICATE)], From 4afb9e1532574b6b6b5ef07680e58431e6281153 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Tue, 23 Jun 2026 19:07:56 +0530 Subject: [PATCH 07/15] refactor(oid4vc): resolve security warnings, harden schema validation, and fix linting Signed-off-by: Sagar Khole --- .../helpers/credential-sessions.builder.ts | 26 +++++++++++++---- .../src/oid4vc-issuance.service.ts | 28 +++++++++++++++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index bf8d1b6a0..a8f3c167b 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -539,17 +539,33 @@ function buildJwtVcJsonLdCredential( const vct = (templateRecord.attributes as any)?.vct; let typeName = ''; if ('string' === typeof vct) { - const lastSlash = vct.lastIndexOf('/'); - typeName = -1 !== lastSlash ? vct.substring(lastSlash + 1) : vct; + const cleanVct = vct.replace(/\/+$/, ''); + const lastSlash = cleanVct.lastIndexOf('/'); + typeName = -1 !== lastSlash ? cleanVct.substring(lastSlash + 1) : cleanVct; } else { typeName = templateRecord.name.replace(/\s+/g, ''); } const templateContext = (templateRecord.attributes as any)?.context; - const context = Array.isArray(templateContext) ? templateContext : ['https://www.w3.org/2018/credentials/v1']; + const context = Array.isArray(templateContext) ? [...templateContext] : ['https://www.w3.org/2018/credentials/v1']; + const requiredContextUrl = 'https://www.w3.org/2018/credentials/v1'; + const normalizeUrlForComparison = (value: unknown): string | null => { + if ('string' !== typeof value) { + return null; + } + try { + return new URL(value).toString(); + } catch { + return null; + } + }; + const normalizedRequiredContext = normalizeUrlForComparison(requiredContextUrl); + const hasRequiredContext = context.some( + (ctx) => null !== normalizedRequiredContext && normalizeUrlForComparison(ctx) === normalizedRequiredContext + ); - if (!context.includes('https://www.w3.org/2018/credentials/v1')) { - context.unshift('https://www.w3.org/2018/credentials/v1'); + if (!hasRequiredContext) { + context.unshift(requiredContextUrl); } const wrappedPayload: Record = { diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index d8d7d0caf..35a3fb35b 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -98,10 +98,28 @@ export class Oid4vcIssuanceService { let internalSchemaId: string | undefined; if ('schemaUrl' in template && template.schemaUrl) { - const parts = template.schemaUrl.split('/'); - const potentialUuid = parts[parts.length - 1]; - if (uuidRegex.test(potentialUuid)) { - internalSchemaId = potentialUuid; + let isInternal = false; + try { + const schemaUrlObj = new URL(template.schemaUrl); + const serverUrlStr = process.env.SCHEMA_FILE_SERVER_URL; + if (serverUrlStr) { + const serverUrlObj = new URL(serverUrlStr); + if (schemaUrlObj.hostname === serverUrlObj.hostname) { + isInternal = true; + } + } + } catch (error) { + if (process.env.SCHEMA_FILE_SERVER_URL && template.schemaUrl.startsWith(process.env.SCHEMA_FILE_SERVER_URL)) { + isInternal = true; + } + } + + if (isInternal) { + const parts = template.schemaUrl.split('/'); + const potentialUuid = parts[parts.length - 1]; + if (uuidRegex.test(potentialUuid)) { + internalSchemaId = potentialUuid; + } } } @@ -113,7 +131,7 @@ export class Oid4vcIssuanceService { .toPromise(); } catch (error) { this.logger.error(`Error fetching schema ${internalSchemaId} from ledger: ${error.message}`); - return; + throw new NotFoundException(`Schema with ID ${internalSchemaId} not found or unreachable: ${error.message}`); } if (schemaResponse && schemaResponse.attributes) { From 40c041222ed03f5c89e5e8a4e75dc0f8978a0890 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Wed, 20 May 2026 15:44:14 +0530 Subject: [PATCH 08/15] feat(oid4vc): add support for jwt_vc_json-ld credential format Signed-off-by: Sagar Khole --- .../dtos/issuer-sessions.dto.ts | 6 +- .../dtos/oid4vc-issuer-template.dto.ts | 9 +- .../oid4vc-issuer-sessions.interfaces.ts | 3 +- .../helpers/credential-sessions.builder.ts | 109 +++++++++++++++++- .../libs/helpers/issuer.metadata.ts | 47 +++++++- libs/enum/src/enum.ts | 3 +- 6 files changed, 161 insertions(+), 16 deletions(-) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts index 32e5e34ab..bb29994f2 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts @@ -274,11 +274,13 @@ export class CredentialDto { @ApiProperty({ description: 'Credential format type', - enum: ['mso_mdoc', 'vc+sd-jwt'], + enum: ['mso_mdoc', 'vc+sd-jwt', 'jwt_vc_json-ld'], example: 'mso_mdoc' }) @IsString() - @IsIn(['mso_mdoc', 'vc+sd-jwt'], { message: 'format must be either "mso_mdoc" or "vc+sd-jwt"' }) + @IsIn(['mso_mdoc', 'vc+sd-jwt', 'jwt_vc_json-ld'], { + message: 'format must be either "mso_mdoc", "vc+sd-jwt" or "jwt_vc_json-ld"' + }) format: string; @ApiProperty({ diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts index a4300ee2a..b8ee8e5ca 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts @@ -229,8 +229,11 @@ export class CreateCredentialTemplateDto { @IsEnum(CredentialFormat) format: CredentialFormat; - @ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.SdJwtVc === o.format) - @IsEmpty({ message: 'doctype must not be provided when format is "dc+sd-jwt"' }) + @ValidateIf( + (o: CreateCredentialTemplateDto) => + CredentialFormat.SdJwtVc === o.format || CredentialFormat.JwtVcJsonLd === o.format + ) + @IsEmpty({ message: 'doctype must not be provided when format is "dc+sd-jwt" or "jwt_vc_json-ld"' }) readonly _doctypeAbsentGuard?: unknown; @ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.Mdoc === o.format) @@ -246,7 +249,7 @@ export class CreateCredentialTemplateDto { @Type(({ object }) => { if (object.format === CredentialFormat.Mdoc) { return MdocTemplateDto; - } else if (object.format === CredentialFormat.SdJwtVc) { + } else if (object.format === CredentialFormat.SdJwtVc || object.format === CredentialFormat.JwtVcJsonLd) { return SdJwtTemplateDto; } }) diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts index 9de39a0a8..a9334383e 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts @@ -5,7 +5,8 @@ import { OpenId4VcIssuanceSessionState } from '@credebl/enum/enum'; * --------------------------------------------------------- */ export enum CredentialFormat { SdJwtVc = 'vc+sd-jwt', - MsoMdoc = 'mso_mdoc' + MsoMdoc = 'mso_mdoc', + JwtVcJsonLd = 'jwt_vc_json-ld' } export enum SignerMethodOption { diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 3fee5d82a..6b187ba07 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -150,14 +150,23 @@ function mapDbFormatToApiFormat(dbFormat: string): CredentialFormat { if (['sd-jwt', 'dc+sd-jwt', 'vc+sd-jwt', 'sdjwt', 'sd+jwt-vc'].includes(normalized)) { return CredentialFormat.SdJwtVc; } + if (['jwt_vc_json-ld', 'jwt-vc-json-ld', 'w3c-jwt-json-ld'].includes(normalized)) { + return CredentialFormat.JwtVcJsonLd; + } if ('mso_mdoc' === normalized || 'mso-mdoc' === normalized || 'mdoc' === normalized) { return CredentialFormat.Mdoc; } throw new UnprocessableEntityException(`Unsupported template format: ${dbFormat}`); } -function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' { - return apiFormat === CredentialFormat.SdJwtVc ? 'sdjwt' : 'mdoc'; +function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' | 'jwt-vc-json-ld' { + if (apiFormat === CredentialFormat.SdJwtVc) { + return 'sdjwt'; + } + if (apiFormat === CredentialFormat.JwtVcJsonLd) { + return 'jwt-vc-json-ld'; + } + return 'mdoc'; } export function buildCredentialOfferUrl(baseUrl: string, getAllCredentialOffer: GetAllCredentialOffer): string { @@ -213,7 +222,7 @@ export function validatePayloadAgainstTemplate(template: any, payload: any): { v } }; - if (CredentialFormat.SdJwtVc === template.format) { + if (CredentialFormat.SdJwtVc === template.format || CredentialFormat.JwtVcJsonLd === template.format) { validateAttributes((template.attributes as SdJwtTemplate).attributes ?? [], payload); } else if (CredentialFormat.Mdoc === template.format) { const namespaces = payload?.namespaces; @@ -456,6 +465,97 @@ function buildMdocCredential( }; } +function buildJwtVcJsonLdCredential( + credentialRequest: CredentialRequestDtoLike, + templateRecord: CredentialTemplateRecord, + signerOptions: ISignerOption[], + activeCertificateDetails?: X509CertificateRecord[] +): BuiltCredential { + const payloadCopy = { ...(credentialRequest.payload as Record) }; + + let expectedSignerMethod: SignerMethodOption; + if (templateRecord.signerOption === SignerOption.DID) { + expectedSignerMethod = SignerMethodOption.DID; + } else if ( + templateRecord.signerOption === SignerOption.X509_P256 || + templateRecord.signerOption === SignerOption.X509_ED25519 + ) { + expectedSignerMethod = SignerMethodOption.X5C; + } else { + throw new UnprocessableEntityException( + `Unknown signer option "${templateRecord.signerOption}" for template ${templateRecord.id}` + ); + } + + const templateSignerOption: ISignerOption | undefined = signerOptions?.find((x) => x.method === expectedSignerMethod); + if (!templateSignerOption) { + throw new UnprocessableEntityException( + `Signer option "${expectedSignerMethod}" is not configured for template ${templateRecord.id}` + ); + } + + if (expectedSignerMethod === SignerMethodOption.X5C && credentialRequest.validityInfo) { + if (!activeCertificateDetails?.length) { + throw new UnprocessableEntityException('Active x.509 certificate details are required for x5c signer templates.'); + } + const certificateDetail = activeCertificateDetails.find( + (x) => x.certificateBase64 === templateSignerOption.x5c?.[0] + ); + if (!certificateDetail) { + throw new UnprocessableEntityException('No active x.509 certificate matches the configured signer option.'); + } + + const validationResult = validateCredentialDatesInCertificateWindow( + credentialRequest.validityInfo, + certificateDetail + ); + if (!validationResult.isValid) { + throw new UnprocessableEntityException(`${JSON.stringify(validationResult.details)}`); + } + } + + let nbf: number | undefined; + let exp: number | undefined; + + if (credentialRequest.validityInfo) { + const credentialValidFrom = new Date(credentialRequest.validityInfo.validFrom); + const credentialValidTo = new Date(credentialRequest.validityInfo.validUntil); + const isCredentialDurationValid = credentialValidFrom <= credentialValidTo; + if (!isCredentialDurationValid) { + const errorDetails = { + credentialDurationValid: isCredentialDurationValid, + credentialValidFrom: credentialValidFrom.toISOString(), + credentialValidTo: credentialValidTo.toISOString() + }; + throw new UnprocessableEntityException(`${JSON.stringify(errorDetails)}`); + } + nbf = dateToSeconds(credentialValidFrom); + exp = dateToSeconds(credentialValidTo); + } + + const apiFormat = mapDbFormatToApiFormat(templateRecord.format); + const idSuffix = formatSuffix(apiFormat); + const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + + const wrappedPayload: Record = { + credentialSubject: payloadCopy + }; + + if (nbf) { + wrappedPayload.nbf = nbf; + } + if (exp) { + wrappedPayload.exp = exp; + } + + return { + credentialSupportedId, + signerOptions: templateSignerOption ? templateSignerOption : undefined, + format: apiFormat, + payload: wrappedPayload + }; +} + export function buildCredentialOfferPayload( dto: CreateOidcCredentialOfferDtoLike, templates: credential_templates[], @@ -489,6 +589,9 @@ export function buildCredentialOfferPayload( if (apiFormat === CredentialFormat.SdJwtVc) { return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); } + if (apiFormat === CredentialFormat.JwtVcJsonLd) { + return buildJwtVcJsonLdCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); + } if (apiFormat === CredentialFormat.Mdoc) { return buildMdocCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); } diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index 9a23223e3..1aafb7d09 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -266,9 +266,7 @@ function buildClaimsFromTemplate(template: SdJwtTemplate | MdocTemplate): Claim[ return claims; } -//TODO: Fix this eslint issue -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate) { +export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate): Record { const formatSuffix = 'sdjwt'; // Determine the unique key for this credential configuration @@ -297,8 +295,7 @@ export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate }; } -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildMdocCredentialConfig(name: string, template: MdocTemplate) { +export function buildMdocCredentialConfig(name: string, template: MdocTemplate): Record { //const claims: Claim[] = []; const formatSuffix = 'mdoc'; @@ -332,12 +329,46 @@ export function buildMdocCredentialConfig(name: string, template: MdocTemplate) }; } +export function buildJwtVcJsonLdCredentialConfig( + name: string, + template: SdJwtTemplate +): Record { + const formatSuffix = 'jwt-vc-json-ld'; + + // Determine the unique key for this credential configuration + const configKey = `${name}-${formatSuffix}`; + const credentialScope = `openid4vc:${template.vct}-${formatSuffix}`; + + const claims = buildClaimsFromTemplate(template); + + return { + [configKey]: { + format: CredentialFormat.JwtVcJsonLd, + scope: credentialScope, + vct: template.vct, + credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS_FOR_SDJWT], + cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS_FOR_SDJWT], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: ['ES256', 'EdDSA'] + } + }, + credential_metadata: { + claims, + display: [] + } + } + }; +} + //TODO: Fix this eslint issue // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function buildCredentialConfig(name: string, template: SdJwtTemplate | MdocTemplate, format: CredentialFormat) { switch (format) { case CredentialFormat.SdJwtVc: return buildSdJwtCredentialConfig(name, template as SdJwtTemplate); + case CredentialFormat.JwtVcJsonLd: + return buildJwtVcJsonLdCredentialConfig(name, template as SdJwtTemplate); case CredentialFormat.Mdoc: return buildMdocCredentialConfig(name, template as MdocTemplate); default: @@ -361,7 +392,11 @@ export function buildCredentialConfigurationsSupported(templateRows: any): Recor const credentialConfig = buildCredentialConfig( templateRow.name, templateToBuild, - format === CredentialFormat.Mdoc ? CredentialFormat.Mdoc : CredentialFormat.SdJwtVc + format === CredentialFormat.Mdoc + ? CredentialFormat.Mdoc + : format === CredentialFormat.JwtVcJsonLd + ? CredentialFormat.JwtVcJsonLd + : CredentialFormat.SdJwtVc ); const appearanceJson = coerceJsonObject(templateRow.appearance); diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index e35b8c1bd..e0ec32df2 100755 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -372,7 +372,8 @@ export enum X509ExtendedKeyUsage { export enum CredentialFormat { SdJwtVc = 'dc+sd-jwt', - Mdoc = 'mso_mdoc' + Mdoc = 'mso_mdoc', + JwtVcJsonLd = 'jwt_vc_json-ld' } export enum AttributeType { From 26d7b5cbf3d6c287a949930eacd6297cf4425b7c Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Wed, 20 May 2026 15:46:31 +0530 Subject: [PATCH 09/15] feat(oid4vc): add support for jwt_vc_json-ld credential format Signed-off-by: Sagar Khole --- apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index 1aafb7d09..ce1f145fc 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -266,7 +266,8 @@ function buildClaimsFromTemplate(template: SdJwtTemplate | MdocTemplate): Claim[ return claims; } -export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate): Record { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate) { const formatSuffix = 'sdjwt'; // Determine the unique key for this credential configuration @@ -295,7 +296,8 @@ export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate }; } -export function buildMdocCredentialConfig(name: string, template: MdocTemplate): Record { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildMdocCredentialConfig(name: string, template: MdocTemplate) { //const claims: Claim[] = []; const formatSuffix = 'mdoc'; @@ -329,10 +331,8 @@ export function buildMdocCredentialConfig(name: string, template: MdocTemplate): }; } -export function buildJwtVcJsonLdCredentialConfig( - name: string, - template: SdJwtTemplate -): Record { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildJwtVcJsonLdCredentialConfig(name: string, template: SdJwtTemplate) { const formatSuffix = 'jwt-vc-json-ld'; // Determine the unique key for this credential configuration From 0d1652e5f963efff2c9f7d3e170ec552e3365792 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Thu, 21 May 2026 06:07:48 +0530 Subject: [PATCH 10/15] refactor(oid4vc): fix DTO formatting and nbf/exp verification Signed-off-by: Sagar Khole --- .../libs/helpers/credential-sessions.builder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 6b187ba07..08ffdc116 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -541,10 +541,10 @@ function buildJwtVcJsonLdCredential( credentialSubject: payloadCopy }; - if (nbf) { + if (nbf !== undefined) { wrappedPayload.nbf = nbf; } - if (exp) { + if (exp !== undefined) { wrappedPayload.exp = exp; } From 6211dd00caf41248d1f5f3417fb264981b5ac4d5 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Fri, 5 Jun 2026 08:14:33 +0530 Subject: [PATCH 11/15] feat(oid4vc): fix jwt_vc_json-ld support and add holder service Signed-off-by: Sagar Khole --- apps/api-gateway/src/app.module.ts | 3 + .../oid4vc-holder/dtos/oid4vc-holder.dto.ts | 40 ++++ .../oid4vc-holder/oid4vc-holder.controller.ts | 128 ++++++++++ .../src/oid4vc-holder/oid4vc-holder.module.ts | 28 +++ .../oid4vc-holder/oid4vc-holder.service.ts | 40 ++++ .../dtos/oid4vc-issuer-template.dto.ts | 8 + .../oid4vc-issuance/oid4vc-issuance.module.ts | 2 +- .../dtos/oid4vc-verifier-presentation.dto.ts | 12 +- .../src/interfaces/oid4vc-holder.interface.ts | 17 ++ apps/oid4vc-holder/src/main.ts | 27 +++ .../src/oid4vc-holder.controller.ts | 46 ++++ .../oid4vc-holder/src/oid4vc-holder.module.ts | 38 +++ .../src/oid4vc-holder.service.ts | 102 ++++++++ apps/oid4vc-issuance/constant/issuance.ts | 3 + .../interfaces/oid4vc-template.interfaces.ts | 1 + .../helpers/credential-sessions.builder.ts | 24 ++ .../libs/helpers/issuer.metadata.ts | 225 ++++++++---------- .../src/oid4vc-issuance.service.ts | 87 ++++++- libs/common/src/common.constant.ts | 13 + libs/common/src/common.utils.ts | 21 +- 20 files changed, 726 insertions(+), 139 deletions(-) create mode 100644 apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts create mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts create mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts create mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts create mode 100644 apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts create mode 100644 apps/oid4vc-holder/src/main.ts create mode 100644 apps/oid4vc-holder/src/oid4vc-holder.controller.ts create mode 100644 apps/oid4vc-holder/src/oid4vc-holder.module.ts create mode 100644 apps/oid4vc-holder/src/oid4vc-holder.service.ts diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 42076ad14..6d94a7cda 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -24,7 +24,9 @@ import { IssuanceModule } from './issuance/issuance.module'; import { LoggerModule } from '@credebl/logger/logger.module'; import { NotificationModule } from './notification/notification.module'; import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; +import { Oid4vcHolderModule } from './oid4vc-holder/oid4vc-holder.module'; import { Oid4vpModule } from './oid4vc-verification/oid4vc-verification.module'; + import { OrganizationModule } from './organization/organization.module'; import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { PlatformModule } from './platform/platform.module'; @@ -77,6 +79,7 @@ import { shouldLoadOidcModules } from '@credebl/common/common.utils'; GeoLocationModule, CloudWalletModule, ConditionalModule.registerWhen(Oid4vcIssuanceModule, shouldLoadOidcModules), + ConditionalModule.registerWhen(Oid4vcHolderModule, shouldLoadOidcModules), ConditionalModule.registerWhen(Oid4vpModule, shouldLoadOidcModules), ConditionalModule.registerWhen(X509Module, shouldLoadOidcModules), KeycloakConfigModule diff --git a/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts b/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts new file mode 100644 index 000000000..8ca580a57 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts @@ -0,0 +1,40 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class OidcResolveCredentialOfferDto { + @ApiProperty({ example: 'openid-credential-offer://?credential_offer_uri=...' }) + @IsString() + @IsNotEmpty() + credentialOfferUri: string; +} + +export class OidcRequestCredentialDto { + @ApiProperty({ example: 'openid-credential-offer://?credential_offer_uri=...' }) + @IsString() + @IsNotEmpty() + credentialOfferUri: string; + + @ApiProperty({ example: ['UniversityDegree'] }) + @IsArray() + @IsNotEmpty() + credentialsToRequest: string[]; + + @ApiPropertyOptional({ example: '1234' }) + @IsString() + @IsOptional() + txCode?: string; +} + +export class OidcResolveProofRequestDto { + @ApiProperty({ example: 'openid-vc-request://?request_uri=...' }) + @IsString() + @IsNotEmpty() + proofRequestUri: string; +} + +export class OidcAcceptProofRequestDto { + @ApiProperty({ example: 'openid-vc-request://?request_uri=...' }) + @IsString() + @IsNotEmpty() + proofRequestUri: string; +} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts new file mode 100644 index 000000000..1c22355b4 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts @@ -0,0 +1,128 @@ +import { Controller, Post, Body, UseGuards, HttpStatus, Res, Param, UseFilters, Logger } from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiForbiddenResponse, + ApiUnauthorizedResponse +} from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { Response } from 'express'; +import { IResponse } from '@credebl/common/interfaces/response.interface'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +import { Oid4vcHolderService } from './oid4vc-holder.service'; +import { + OidcAcceptProofRequestDto, + OidcRequestCredentialDto, + OidcResolveCredentialOfferDto, + OidcResolveProofRequestDto +} from './dtos/oid4vc-holder.dto'; + +@Controller('orgs/:orgId/oid4vc/holder') +@UseFilters(CustomExceptionFilter) +@ApiTags('OID4VC-Holder') +@ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) +export class Oid4vcHolderController { + private readonly logger = new Logger('Oid4vcHolderController'); + constructor(private readonly oid4vcHolderService: Oid4vcHolderService) {} + + @Post('resolve-credential-offer') + @ApiOperation({ + summary: 'Resolve OID4VC Credential Offer', + description: 'Resolves an OID4VC credential offer for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credential offer resolved successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcHolderResolveCredentialOffer( + @Param('orgId') orgId: string, + @Body() resolveDto: OidcResolveCredentialOfferDto, + @Res() res: Response + ): Promise { + const data = await this.oid4vcHolderService.oidcHolderResolveCredentialOffer(orgId, resolveDto); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: 'Credential offer resolved successfully.', + data + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('request-credential') + @ApiOperation({ + summary: 'Request OID4VC Credential', + description: 'Requests and stores an OID4VC credential for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credential requested successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcHolderRequestCredential( + @Param('orgId') orgId: string, + @Body() requestDto: OidcRequestCredentialDto, + @Res() res: Response + ): Promise { + const data = await this.oid4vcHolderService.oidcHolderRequestCredential(orgId, requestDto); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: 'Credential requested successfully.', + data + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('resolve-proof-request') + @ApiOperation({ + summary: 'Resolve OID4VC Proof Request', + description: 'Resolves an OID4VC proof request for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Proof request resolved successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcHolderResolveProofRequest( + @Param('orgId') orgId: string, + @Body() resolveDto: OidcResolveProofRequestDto, + @Res() res: Response + ): Promise { + const data = await this.oid4vcHolderService.oidcHolderResolveProofRequest(orgId, resolveDto); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: 'Proof request resolved successfully.', + data + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('accept-proof-request') + @ApiOperation({ + summary: 'Accept OID4VC Proof Request', + description: 'Accepts an OID4VC proof request for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Proof request accepted successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcHolderAcceptProofRequest( + @Param('orgId') orgId: string, + @Body() acceptDto: OidcAcceptProofRequestDto, + @Res() res: Response + ): Promise { + const data = await this.oid4vcHolderService.oidcHolderAcceptProofRequest(orgId, acceptDto); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: 'Proof request accepted successfully.', + data + }; + return res.status(HttpStatus.OK).json(finalResponse); + } +} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts new file mode 100644 index 000000000..5cb87b1ee --- /dev/null +++ b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts @@ -0,0 +1,28 @@ +import { Module } from '@nestjs/common'; +import { NATSClient } from '@credebl/common/NATSClient'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { HttpModule } from '@nestjs/axios'; +import { Oid4vcHolderController } from './oid4vc-holder.controller'; +import { Oid4vcHolderService } from './oid4vc-holder.service'; + +@Module({ + imports: [ + HttpModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions( + CommonConstants.OIDC4VC_HOLDER_SERVICE, + process.env.API_GATEWAY_NKEY_SEED, + process.env.NATS_CREDS_FILE + ) + } + ]) + ], + controllers: [Oid4vcHolderController], + providers: [Oid4vcHolderService, NATSClient] +}) +export class Oid4vcHolderModule {} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts new file mode 100644 index 000000000..bdd976488 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts @@ -0,0 +1,40 @@ +import { NATSClient } from '@credebl/common/NATSClient'; +import { Inject, Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +import { + OidcAcceptProofRequestDto, + OidcRequestCredentialDto, + OidcResolveCredentialOfferDto, + OidcResolveProofRequestDto +} from './dtos/oid4vc-holder.dto'; + +@Injectable() +export class Oid4vcHolderService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly holderProxy: ClientProxy, + private readonly natsClient: NATSClient + ) { + super('Oid4vcHolderService'); + } + + async oidcHolderResolveCredentialOffer(orgId: string, holderPayload: OidcResolveCredentialOfferDto): Promise { + const payload = { orgId, holderPayload }; + return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-resolve-credential-offer', payload); + } + + async oidcHolderRequestCredential(orgId: string, holderPayload: OidcRequestCredentialDto): Promise { + const payload = { orgId, holderPayload }; + return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-request-credential', payload); + } + + async oidcHolderResolveProofRequest(orgId: string, holderPayload: OidcResolveProofRequestDto): Promise { + const payload = { orgId, holderPayload }; + return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-resolve-proof-request', payload); + } + + async oidcHolderAcceptProofRequest(orgId: string, holderPayload: OidcAcceptProofRequestDto): Promise { + const payload = { orgId, holderPayload }; + return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-accept-proof-request', payload); + } +} diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts index b8ee8e5ca..7649c002c 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts @@ -196,6 +196,14 @@ export class SdJwtTemplateDto { @IsString() vct: string; + @ApiPropertyOptional({ + example: ['https://www.w3.org/2018/credentials/v1', 'https://json-ld.org/contexts/person.jsonld'], + description: 'JSON-LD context for the credential (optional, defaults to person schema for JSON-LD format)' + }) + @IsArray() + @IsOptional() + context?: string[]; + @ApiProperty({ type: 'array', items: { $ref: getSchemaPath(CredentialAttributeDto) }, diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts index f6833f4e8..ec5e70f91 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts @@ -15,7 +15,7 @@ import { HttpModule } from '@nestjs/axios'; name: 'NATS_CLIENT', transport: Transport.NATS, options: getNatsOptions( - CommonConstants.ISSUANCE_SERVICE, + CommonConstants.OIDC4VC_ISSUANCE_SERVICE, process.env.API_GATEWAY_NKEY_SEED, process.env.NATS_CREDS_FILE ) diff --git a/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts b/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts index c26e004ec..9c85706ca 100644 --- a/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts +++ b/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts @@ -289,7 +289,7 @@ export class DcqlDto { @ValidatorConstraint({ name: 'OnlyOneOf', async: false }) export class OnlyOneOfConstraint implements ValidatorConstraintInterface { // eslint-disable-next-line @typescript-eslint/no-explicit-any - validate(_: any, args: ValidationArguments): Promise | boolean { + validate(_: unknown, args: ValidationArguments): Promise | boolean { // eslint-disable-next-line @typescript-eslint/no-explicit-any const object = args.object as Record; const properties = args.constraints as string[]; @@ -356,10 +356,18 @@ export class PresentationRequestDto { @IsDefined() @IsEnum(ResponseMode) responseMode: ResponseMode; - //TODO: check e2e flow and add ResponseMode based restrictions @IsOptional() expectedOrigins: string[]; + @ApiPropertyOptional({ + description: 'OpenID4VP version to use', + enum: ['v1', 'v1.draft21', 'v1.draft24'], + example: 'v1.draft24' + }) + @IsOptional() + @IsEnum(['v1', 'v1.draft21', 'v1.draft24']) + version?: 'v1' | 'v1.draft21' | 'v1.draft24'; + /** * Dummy property used to run a class-level validation ensuring mutual exclusivity. * This property is not serialized into requests/responses but is required so `class-validator` diff --git a/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts b/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts new file mode 100644 index 000000000..fc2f0e48b --- /dev/null +++ b/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts @@ -0,0 +1,17 @@ +export interface IOidcHolderResolveCredentialOffer { + credentialOfferUri: string; +} + +export interface IOidcHolderRequestCredential { + credentialOfferUri: string; + credentialsToRequest: string[]; + txCode?: string; +} + +export interface IOidcHolderResolveProofRequest { + proofRequestUri: string; +} + +export interface IOidcHolderAcceptProofRequest { + proofRequestUri: string; +} diff --git a/apps/oid4vc-holder/src/main.ts b/apps/oid4vc-holder/src/main.ts new file mode 100644 index 000000000..5e9310dd1 --- /dev/null +++ b/apps/oid4vc-holder/src/main.ts @@ -0,0 +1,27 @@ +import { NestFactory } from '@nestjs/core'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { Oid4vcHolderModule } from './oid4vc-holder.module'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice(Oid4vcHolderModule, { + transport: Transport.NATS, + options: getNatsOptions( + CommonConstants.OIDC4VC_HOLDER_SERVICE, + process.env.OIDC4VC_HOLDER_NKEY_SEED, + process.env.NATS_CREDS_FILE + ) + }); + app.useLogger(app.get(NestjsLoggerServiceAdapter)); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('OID4VC-Holder-Service Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/oid4vc-holder/src/oid4vc-holder.controller.ts b/apps/oid4vc-holder/src/oid4vc-holder.controller.ts new file mode 100644 index 000000000..802a20133 --- /dev/null +++ b/apps/oid4vc-holder/src/oid4vc-holder.controller.ts @@ -0,0 +1,46 @@ +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { Oid4vcHolderService } from './oid4vc-holder.service'; +import { + IOidcHolderAcceptProofRequest, + IOidcHolderRequestCredential, + IOidcHolderResolveCredentialOffer, + IOidcHolderResolveProofRequest +} from './interfaces/oid4vc-holder.interface'; + +@Controller() +export class Oid4vcHolderController { + constructor(private readonly oid4vcHolderService: Oid4vcHolderService) {} + + @MessagePattern({ cmd: 'oid4vc-holder-resolve-credential-offer' }) + async oidcHolderResolveCredentialOffer(payload: { + orgId: string; + holderPayload: IOidcHolderResolveCredentialOffer; + }): Promise { + return this.oid4vcHolderService.oidcHolderResolveCredentialOffer(payload.orgId, payload.holderPayload); + } + + @MessagePattern({ cmd: 'oid4vc-holder-request-credential' }) + async oidcHolderRequestCredential(payload: { + orgId: string; + holderPayload: IOidcHolderRequestCredential; + }): Promise { + return this.oid4vcHolderService.oidcHolderRequestCredential(payload.orgId, payload.holderPayload); + } + + @MessagePattern({ cmd: 'oid4vc-holder-resolve-proof-request' }) + async oidcHolderResolveProofRequest(payload: { + orgId: string; + holderPayload: IOidcHolderResolveProofRequest; + }): Promise { + return this.oid4vcHolderService.oidcHolderResolveProofRequest(payload.orgId, payload.holderPayload); + } + + @MessagePattern({ cmd: 'oid4vc-holder-accept-proof-request' }) + async oidcHolderAcceptProofRequest(payload: { + orgId: string; + holderPayload: IOidcHolderAcceptProofRequest; + }): Promise { + return this.oid4vcHolderService.oidcHolderAcceptProofRequest(payload.orgId, payload.holderPayload); + } +} diff --git a/apps/oid4vc-holder/src/oid4vc-holder.module.ts b/apps/oid4vc-holder/src/oid4vc-holder.module.ts new file mode 100644 index 000000000..0d89112b7 --- /dev/null +++ b/apps/oid4vc-holder/src/oid4vc-holder.module.ts @@ -0,0 +1,38 @@ +import { Module, Logger } from '@nestjs/common'; +import { Oid4vcHolderController } from './oid4vc-holder.controller'; +import { Oid4vcHolderService } from './oid4vc-holder.service'; +import { CommonModule } from '@credebl/common'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { LoggerModule } from '@credebl/logger'; +import { PrismaServiceModule } from '@credebl/prisma-service'; +import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; +import { GlobalConfigModule } from '@credebl/config'; +import { ContextInterceptorModule } from '@credebl/context'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { NATSClient } from '@credebl/common/NATSClient'; + +@Module({ + imports: [ + CommonModule, + LoggerModule, + PrismaServiceModule, + PlatformConfig, + GlobalConfigModule, + ContextInterceptorModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions( + CommonConstants.OIDC4VC_HOLDER_SERVICE, + process.env.OIDC4VC_HOLDER_NKEY_SEED, + process.env.NATS_CREDS_FILE + ) + } + ]) + ], + controllers: [Oid4vcHolderController], + providers: [Oid4vcHolderService, NATSClient, Logger] +}) +export class Oid4vcHolderModule {} diff --git a/apps/oid4vc-holder/src/oid4vc-holder.service.ts b/apps/oid4vc-holder/src/oid4vc-holder.service.ts new file mode 100644 index 000000000..c5c156df5 --- /dev/null +++ b/apps/oid4vc-holder/src/oid4vc-holder.service.ts @@ -0,0 +1,102 @@ +import { Injectable, Logger, Inject } from '@nestjs/common'; +import { CommonService } from '@credebl/common'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { PrismaService } from '@credebl/prisma-service'; +import { ClientProxy } from '@nestjs/microservices'; +import { NATSClient } from '@credebl/common/NATSClient'; +import { getAgentUrl } from '@credebl/common/common.utils'; +import { + IOidcHolderAcceptProofRequest, + IOidcHolderRequestCredential, + IOidcHolderResolveCredentialOffer, + IOidcHolderResolveProofRequest +} from './interfaces/oid4vc-holder.interface'; + +@Injectable() +export class Oid4vcHolderService { + private readonly logger = new Logger('Oid4vcHolderService'); + + constructor( + private readonly commonService: CommonService, + private readonly prisma: PrismaService, + @Inject('NATS_CLIENT') private readonly natsClientProxy: ClientProxy, + private readonly natsClient: NATSClient + ) {} + + private async getOrgAgentApiKey(orgId: string): Promise { + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { orgId } + }); + if (!agentDetails || !agentDetails.apiKey) { + throw new Error(`Agent API key not found for org: ${orgId}`); + } + const decryptedToken = await this.commonService.decryptPassword(agentDetails.apiKey); + return decryptedToken; + } + + async _getAgentEndpoint(orgId: string): Promise { + const payload = { orgId }; + const agentDetails = await this.natsClient.sendNatsMessage( + this.natsClientProxy, + 'get-agent-details-by-org-id', + payload + ); + return (agentDetails as { agentEndPoint: string }).agentEndPoint; + } + + async oidcHolderResolveCredentialOffer(orgId: string, payload: IOidcHolderResolveCredentialOffer): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_RESOLVE_OFFER); + const data = await this.commonService + .httpPost(url, payload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcHolderResolveCredentialOffer in holder service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcHolderRequestCredential(orgId: string, payload: IOidcHolderRequestCredential): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_REQUEST_CREDENTIAL); + const data = await this.commonService + .httpPost(url, payload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcHolderRequestCredential in holder service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcHolderResolveProofRequest(orgId: string, payload: IOidcHolderResolveProofRequest): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_RESOLVE_PROOF_REQUEST); + const data = await this.commonService + .httpPost(url, payload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcHolderResolveProofRequest in holder service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcHolderAcceptProofRequest(orgId: string, payload: IOidcHolderAcceptProofRequest): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_ACCEPT_PROOF_REQUEST); + const data = await this.commonService + .httpPost(url, payload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcHolderAcceptProofRequest in holder service : ${JSON.stringify(error)}`); + throw error; + } + } +} diff --git a/apps/oid4vc-issuance/constant/issuance.ts b/apps/oid4vc-issuance/constant/issuance.ts index c1adf5aa2..2865e3c6c 100644 --- a/apps/oid4vc-issuance/constant/issuance.ts +++ b/apps/oid4vc-issuance/constant/issuance.ts @@ -7,3 +7,6 @@ export const accessTokenSignerKeyType = { kty: 'OKP', crv: 'Ed25519' } as { crv: AccessTokenSignerKeyType; }; export const batchCredentialIssuanceDefault = 0; + +export const CREDENTIALS_CONTEXT_V1_URL = 'https://www.w3.org/2018/credentials/v1'; +export const CREDENTIALS_CONTEXT_V2_URL = 'https://www.w3.org/ns/credentials/v2'; diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts index e6a6ce61b..f1b235de2 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -2,6 +2,7 @@ import { Prisma, SignerOption } from '@prisma/client'; import { AttributeType, CredentialFormat } from '@credebl/enum/enum'; export interface SdJwtTemplate { vct: string; + context?: string[]; attributes: CredentialAttribute[]; } diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 08ffdc116..bf8d1b6a0 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -536,8 +536,25 @@ function buildJwtVcJsonLdCredential( const apiFormat = mapDbFormatToApiFormat(templateRecord.format); const idSuffix = formatSuffix(apiFormat); const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + const vct = (templateRecord.attributes as any)?.vct; + let typeName = ''; + if ('string' === typeof vct) { + const lastSlash = vct.lastIndexOf('/'); + typeName = -1 !== lastSlash ? vct.substring(lastSlash + 1) : vct; + } else { + typeName = templateRecord.name.replace(/\s+/g, ''); + } + + const templateContext = (templateRecord.attributes as any)?.context; + const context = Array.isArray(templateContext) ? templateContext : ['https://www.w3.org/2018/credentials/v1']; + + if (!context.includes('https://www.w3.org/2018/credentials/v1')) { + context.unshift('https://www.w3.org/2018/credentials/v1'); + } const wrappedPayload: Record = { + '@context': context, + type: ['VerifiableCredential', typeName], credentialSubject: payloadCopy }; @@ -548,6 +565,13 @@ function buildJwtVcJsonLdCredential( wrappedPayload.exp = exp; } + if (credentialRequest.validityInfo) { + wrappedPayload.issuanceDate = new Date(credentialRequest.validityInfo.validFrom).toISOString(); + wrappedPayload.expirationDate = new Date(credentialRequest.validityInfo.validUntil).toISOString(); + } else { + wrappedPayload.issuanceDate = new Date().toISOString(); + } + return { credentialSupportedId, signerOptions: templateSignerOption ? templateSignerOption : undefined, diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index ce1f145fc..f252330b5 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -3,7 +3,13 @@ import { oidc_issuer, Prisma } from '@prisma/client'; import { batchCredentialIssuanceDefault } from '../../constant/issuance'; import { CreateOidcCredentialOffer } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; import { IssuerResponse } from 'apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces'; -import { Claim, MdocTemplate, SdJwtTemplate } from 'apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces'; +import { + Claim, + ClaimDisplay, + CredentialAttribute, + MdocTemplate, + SdJwtTemplate +} from 'apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces'; import { CredentialFormat } from '@credebl/enum/enum'; type AttributeDisplay = { name: string; locale: string }; @@ -30,13 +36,6 @@ type Appearance = { display: CredentialDisplayItem[]; }; -// type Claim = { -// mandatory?: boolean; -// // value_type: string; -// path: string[]; -// display?: AttributeDisplay[]; -// }; - type CredentialConfig = { format: string; vct?: string; @@ -45,6 +44,8 @@ type CredentialConfig = { credential_signing_alg_values_supported: string[] | number[]; cryptographic_binding_methods_supported: string[]; + proof_types_supported?: Record; + credential_definition?: Record; credential_metadata: { claims: Claim[]; @@ -56,110 +57,57 @@ type CredentialConfigurationsSupported = { credentialConfigurationsSupported: Record; }; -// ---- Static Lists (as requested) ---- -const STATIC_CREDENTIAL_ALGS_FOR_SDJWT = ['ES256', 'EdDSA'] as const; -const STATIC_CREDENTIAL_ALGS_FOR_MDOC = [-7, -9] as const; -const STATIC_BINDING_METHODS_FOR_SDJWT = ['did:key', 'did:web', 'did:jwk', 'jwk'] as const; -const STATIC_BINDING_METHODS_FOR_MDOC = ['cose_key'] as const; // We need to test 'did:key', 'did:web', 'did:jwk', 'jwk', +const ISSUER_DPOP_ALGS_DEFAULT = ['RS256', 'ES256']; -// Safe coercion helpers -function coerceJsonObject(v: Prisma.JsonValue): T | null { - if (null == v) { - return null; - } - if ('string' === typeof v) { - try { - return JSON.parse(v) as T; - } catch { - return null; - } - } - return v as unknown as T; // already a JsonObject/JsonArray -} -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// function isAttributesMap(x: any): x is AttributesMap { -// return x && 'object' === typeof x && Array.isArray(x); -// } -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// function isAppearance(x: any): x is Appearance { -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// return x && 'object' === typeof x && Array.isArray((x as any).display); -// } - -// // Prisma row shape -// type TemplateRowPrisma = { -// id: string; -// name: string; -// description?: string | null; -// format?: string | null; -// canBeRevoked?: boolean | null; -// attributes: Prisma.JsonValue; // JsonValue from DB -// appearance: Prisma.JsonValue; // JsonValue from DB -// issuerId: string; -// createdAt?: Date | string; -// updatedAt?: Date | string; -// }; - -// Prisma row shape -//TODO: Fix this eslint issue -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type TemplateRowPrisma = { - id: string; - name: string; - description?: string | null; - format?: string | null; - canBeRevoked?: boolean | null; - attributes: SdJwtTemplate | MdocTemplate; // JsonValue from DB - appearance: Prisma.JsonValue; // JsonValue from DB - issuerId: string; - createdAt?: Date | string; - updatedAt?: Date | string; -}; +const STATIC_CREDENTIAL_ALGS_FOR_SDJWT = ['ES256']; +const STATIC_BINDING_METHODS_FOR_SDJWT = ['did:key', 'did:web', 'did:jwk', 'jwk']; -// Default DPoP list for issuer-level metadata (match your example) -const ISSUER_DPOP_ALGS_DEFAULT = ['RS256', 'ES256'] as const; +const STATIC_CREDENTIAL_ALGS_FOR_MDOC = ['ES256']; +const STATIC_BINDING_METHODS_FOR_MDOC = ['jwk']; -// ---------- Safe coercion ---------- -function coerceJson(v: Prisma.JsonValue): T | null { - if (null == v) { - return null; - } - if ('string' === typeof v) { - try { - return JSON.parse(v) as T; - } catch { - return null; - } - } - return v as unknown as T; +/** + * Checks if a value is an array of DisplayItem. + * (Simple runtime check, could be more elaborate) + */ +function isDisplayArray(val: unknown): val is DisplayItem[] { + return Array.isArray(val); } type DisplayItem = { - name: string; + name?: string; locale?: string; - description?: string; - logo?: { uri: string; alt_text?: string }; + logo?: { + uri: string; + alt_text?: string; + }; }; -function isDisplayArray(x: unknown): x is DisplayItem[] { - return ( - Array.isArray(x) && - x.every( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (i) => i && 'object' === typeof i && 'string' === typeof (i as any).name - ) - ); +/** + * Safely coerces a Prisma.JsonValue (from oidc_issuer.metadata) into a JS object. + */ +function coerceJson(val: Prisma.JsonValue): T { + if (null === val || undefined === val) { + return {} as T; + } + if ('string' === typeof val) { + try { + return JSON.parse(val) as T; + } catch { + return {} as T; + } + } + return val as T; } -// ---------- Builder you asked for ---------- /** - * Build issuer metadata payload from issuer row + credential configurations. + * Builder #1: Issuer Metadata Payload + * + * Builds the root OIDC Issuer metadata object (including "display", "dpop...", etc.). * - * @param credentialConfigurations Object with credentialConfigurationsSupported (from your template builder) - * @param oidcIssuer OID4VC issuer row (uses publicIssuerId and metadata -> display) + * @param credentialConfigurations The "credential_configurations_supported" block built by Builder #2. + * @param oidcIssuer The Prisma row for the OIDC Issuer. * @param opts Optional overrides: dpopAlgs[], accessTokenSignerKeyType */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function buildIssuerPayload( credentialConfigurations: CredentialConfigurationsSupported, oidcIssuer: oidc_issuer, @@ -167,7 +115,7 @@ export function buildIssuerPayload( dpopAlgs?: string[]; accessTokenSignerKeyType?: string; } -) { +): Record { if (!oidcIssuer?.publicIssuerId || 'string' !== typeof oidcIssuer.publicIssuerId) { throw new Error('Invalid issuer: missing publicIssuerId'); } @@ -175,14 +123,21 @@ export function buildIssuerPayload( const rawDisplay = coerceJson(oidcIssuer.metadata); const display: DisplayItem[] = isDisplayArray(rawDisplay) ? rawDisplay : []; - return { + const batchSize = oidcIssuer?.batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault; + + const payload: Record = { display, dpopSigningAlgValuesSupported: opts?.dpopAlgs ?? [...ISSUER_DPOP_ALGS_DEFAULT], - credentialConfigurationsSupported: credentialConfigurations.credentialConfigurationsSupported ?? {}, - batchCredentialIssuance: { - batchSize: oidcIssuer?.batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault - } + credentialConfigurationsSupported: credentialConfigurations.credentialConfigurationsSupported ?? {} }; + + if (0 < batchSize) { + payload.batchCredentialIssuance = { + batchSize + }; + } + + return payload; } export function extractTemplateIds(offer: CreateOidcCredentialOffer): string[] { @@ -211,7 +166,7 @@ export function encodeIssuerPublicId(publicIssuerId: string): string { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -function generateClaims(attributes: any[], namespace?: string, parentPath: string[] = []): Claim[] { +function generateClaims(attributes: CredentialAttribute[], namespace?: string, parentPath: string[] = []): Claim[] { const result: Claim[] = []; for (const attr of attributes) { @@ -222,7 +177,7 @@ function generateClaims(attributes: any[], namespace?: string, parentPath: strin const claim: Claim = { path }; if (attr.display) { - claim.display = attr.display; + claim.display = attr.display as ClaimDisplay[]; } if (true === attr.mandatory) { @@ -267,7 +222,7 @@ function buildClaimsFromTemplate(template: SdJwtTemplate | MdocTemplate): Claim[ } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate) { +export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate): Record { const formatSuffix = 'sdjwt'; // Determine the unique key for this credential configuration @@ -297,7 +252,7 @@ export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildMdocCredentialConfig(name: string, template: MdocTemplate) { +export function buildMdocCredentialConfig(name: string, template: MdocTemplate): Record { //const claims: Claim[] = []; const formatSuffix = 'mdoc'; @@ -332,7 +287,10 @@ export function buildMdocCredentialConfig(name: string, template: MdocTemplate) } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildJwtVcJsonLdCredentialConfig(name: string, template: SdJwtTemplate) { +export function buildJwtVcJsonLdCredentialConfig( + name: string, + template: SdJwtTemplate +): Record { const formatSuffix = 'jwt-vc-json-ld'; // Determine the unique key for this credential configuration @@ -341,6 +299,13 @@ export function buildJwtVcJsonLdCredentialConfig(name: string, template: SdJwtTe const claims = buildClaimsFromTemplate(template); + const { vct } = template; + let typeName = name ? name.replace(/[^a-zA-Z0-9]/g, '') : 'GenericCredential'; + if ('string' === typeof vct && '' !== vct.trim()) { + const lastSlash = vct.lastIndexOf('/'); + typeName = -1 !== lastSlash ? vct.substring(lastSlash + 1) : vct; + } + return { [configKey]: { format: CredentialFormat.JwtVcJsonLd, @@ -353,6 +318,10 @@ export function buildJwtVcJsonLdCredentialConfig(name: string, template: SdJwtTe proof_signing_alg_values_supported: ['ES256', 'EdDSA'] } }, + credential_definition: { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential', typeName] + }, credential_metadata: { claims, display: [] @@ -363,7 +332,11 @@ export function buildJwtVcJsonLdCredentialConfig(name: string, template: SdJwtTe //TODO: Fix this eslint issue // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function buildCredentialConfig(name: string, template: SdJwtTemplate | MdocTemplate, format: CredentialFormat) { +export function buildCredentialConfig( + name: string, + template: SdJwtTemplate | MdocTemplate, + format: CredentialFormat +): Record { switch (format) { case CredentialFormat.SdJwtVc: return buildSdJwtCredentialConfig(name, template as SdJwtTemplate); @@ -380,17 +353,15 @@ export function buildCredentialConfig(name: string, template: SdJwtTemplate | Md * Build agent payload from Prisma rows (attributes/appearance are Prisma.JsonValue). * Safely coerces JSON and then builds the same structure as Builder #2. */ -//TODO: Fix this eslint issue -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function buildCredentialConfigurationsSupported(templateRows: any): Record { +export function buildCredentialConfigurationsSupported(templateRows: unknown[]): Record { const credentialConfigMap: Record = {}; - for (const templateRow of templateRows) { - const { format } = templateRow; - const templateToBuild = templateRow.attributes; + for (const templateRow of templateRows as Record[]) { + const format = templateRow.format as string; + const templateToBuild = templateRow.attributes as SdJwtTemplate | MdocTemplate; const credentialConfig = buildCredentialConfig( - templateRow.name, + templateRow.name as string, templateToBuild, format === CredentialFormat.Mdoc ? CredentialFormat.Mdoc @@ -398,25 +369,15 @@ export function buildCredentialConfigurationsSupported(templateRows: any): Recor ? CredentialFormat.JwtVcJsonLd : CredentialFormat.SdJwtVc ); - const appearanceJson = coerceJsonObject(templateRow.appearance); - // Prepare the display configuration - const displayConfigurations = - (appearanceJson as Appearance).display?.map((displayEntry) => ({ + const appearance = coerceJson(templateRow.appearance as Prisma.JsonValue); + const displayConfigurations: CredentialDisplayItem[] = + appearance.display?.map((displayEntry) => ({ name: displayEntry.name, - description: displayEntry.description, locale: displayEntry.locale, - logo: displayEntry?.logo - ? { - uri: displayEntry.logo.uri, - alt_text: displayEntry.logo.alt_text - } - : undefined, - background_image: displayEntry?.background_image?.uri - ? { - uri: displayEntry.background_image.uri - } - : undefined, + logo: displayEntry.logo, + description: displayEntry.description, + background_image: displayEntry.background_image, background_color: displayEntry.background_color, text_color: displayEntry.text_color })) ?? []; diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 859373257..86111e550 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -40,8 +40,11 @@ import { accessTokenSignerKeyType, batchCredentialIssuanceDefault, credentialConfigurationsSupported, - dpopSigningAlgValuesSupported + dpopSigningAlgValuesSupported, + CREDENTIALS_CONTEXT_V1_URL, + CREDENTIALS_CONTEXT_V2_URL } from '../constant/issuance'; +import { uuidRegex } from '@credebl/common/common.constant'; import { buildCredentialConfigurationsSupported, buildIssuerPayload, @@ -86,6 +89,81 @@ export class Oid4vcIssuanceService { private readonly statusListAllocatorService: StatusListAllocatorService ) {} + private async validateTemplateAgainstSchema( + format: string | CredentialFormat, + template: any, + orgId: string + ): Promise { + if ( + format === CredentialFormat.JwtVcJsonLd && + template && + 'context' in template && + Array.isArray(template.context) + ) { + const contexts = template.context; + let internalSchemaId: string | undefined; + + for (const url of contexts) { + if (url === CREDENTIALS_CONTEXT_V1_URL || url === CREDENTIALS_CONTEXT_V2_URL) { + continue; + } + if (url.includes('/schemas/')) { + const parts = url.split('/'); + const potentialUuid = parts[parts.length - 1]; + if (uuidRegex.test(potentialUuid)) { + internalSchemaId = potentialUuid; + break; + } + } + } + + if (internalSchemaId) { + let schemaResponse; + try { + schemaResponse = await this.issuanceServiceProxy + .send({ cmd: 'get-schema-by-id' }, { schemaId: internalSchemaId, orgId }) + .toPromise(); + } catch (error) { + this.logger.error(`Error fetching schema ${internalSchemaId} from ledger: ${error.message}`); + return; + } + + if (schemaResponse && schemaResponse.attributes) { + const schemaAttributes = + 'string' === typeof schemaResponse.attributes + ? JSON.parse(schemaResponse.attributes) + : schemaResponse.attributes; + const schemaAttributeNames = schemaAttributes.map((attr: any) => attr.attributeName); + const templateAttributes = template.attributes; + + const missingAttributes: string[] = []; + const checkAttributes = (attrs: any[]) => { + if (!attrs) { + return; + } + for (const attr of attrs) { + if (!schemaAttributeNames.includes(attr.key)) { + missingAttributes.push(attr.key); + } + if (attr.children) { + checkAttributes(attr.children); + } + } + }; + checkAttributes(templateAttributes); + + if (0 < missingAttributes.length) { + throw new BadRequestException( + `Attributes [ '${missingAttributes.join("', '")}' ] are not defined in the referenced schema.` + ); + } + } + } else { + this.logger.log('No internal schema reference found; skipping strict attribute validation.'); + } + } + } + async oidcIssuerCreate(issuerCreation: IssuerCreation, orgId: string, userDetails: user): Promise { try { const { issuerId, batchCredentialIssuanceSize } = issuerCreation; @@ -322,6 +400,8 @@ export class Oid4vcIssuanceService { //TODO: add revert mechanism if agent call fails const { name, description, format, canBeRevoked, appearance, signerOption, noticeUrl } = credentialTemplate; + await this.validateTemplateAgainstSchema(format, credentialTemplate.template, orgId); + const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer(name, issuerId); if (0 < checkNameExist.length) { throw new ConflictException(ResponseMessages.oidcTemplate.error.templateNameAlreadyExist); @@ -406,6 +486,11 @@ export class Oid4vcIssuanceService { ...(issuerId ? { issuerId } : {}) }; const { name, description, format, canBeRevoked, appearance, signerOption, noticeUrl } = normalized; + + if (normalized.template && (format || template.format)) { + await this.validateTemplateAgainstSchema(format || template.format, normalized.template, orgId); + } + const attributes = instanceToPlain(normalized.template); const payload = { diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 99d23bb4d..12a4be7b7 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -137,6 +137,12 @@ export enum CommonConstants { URL_OID4VP_VERIFICATION_SESSION = '/openid4vc/verification-sessions/create-presentation-request', URL_OIDC_VERIFIER_SESSION_AUTH_RESPONSE_VERIFY = '/openid4vc/verification-sessions/verify-authorization-response', + // OID4VC Holder URLs + URL_OIDC_HOLDER_RESOLVE_OFFER = '/openid4vc/holder/resolve-credential-offer', + URL_OIDC_HOLDER_REQUEST_CREDENTIAL = '/openid4vc/holder/request-credential', + URL_OIDC_HOLDER_RESOLVE_PROOF_REQUEST = '/openid4vc/holder/resolve-proof-request', + URL_OIDC_HOLDER_ACCEPT_PROOF_REQUEST = '/openid4vc/holder/accept-proof-request', + //X509 agent API URLs URL_CREATE_X509_CERTIFICATE = '/x509', URL_IMPORT_X509_CERTIFICATE = '/x509/import', @@ -397,6 +403,7 @@ export enum CommonConstants { CLOUD_WALLET_SERVICE = 'cloud-wallet', OIDC4VC_ISSUANCE_SERVICE = 'oid4vc-issuance', OIDC4VC_VERIFICATION_SERVICE = 'oid4vc-verification', + OIDC4VC_HOLDER_SERVICE = 'oid4vc-holder', OID4VP_VERIFICATION_SESSION = 'oid4vp-verification-session', X509_SERVICE = 'x509-service', @@ -434,6 +441,12 @@ export enum CommonConstants { OIDC_ISSUER_SESSIONS = 'get-oid4vc-sessions', OIDC_DELETE_CREDENTIAL_OFFER = 'delete-oid4vc-credential-offer', + // OID4VC Holder + OIDC_HOLDER_RESOLVE_OFFER = 'resolve-credential-offer', + OIDC_HOLDER_REQUEST_CREDENTIAL = 'request-credential', + OIDC_HOLDER_RESOLVE_PROOF_REQUEST = 'resolve-proof-request', + OIDC_HOLDER_ACCEPT_PROOF_REQUEST = 'accept-proof-request', + // OID4VP OIDC_VERIFIER_CREATE = 'create-oid4vp-verifier', OIDC_VERIFIER_UPDATE = 'update-oid4vp-verifier', diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index 98f92aeac..708b21074 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -25,9 +25,11 @@ export function paginator(items: T[], current_page: number, items_per_page: n }; } -export function orderValues(key, order = 'asc') { - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types - return function innerSort(a, b) { +export function orderValues( + key: string, + order = 'asc' +): (a: Record, b: Record) => number { + return function innerSort(a: Record, b: Record): number { if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) { // property doesn't exist on either object return 0; @@ -107,6 +109,19 @@ export const getAgentUrl = (agentEndPoint: string, urlFlag: string, paramId?: st [String(CommonConstants.OIDC_ISSUER_SESSIONS_BY_ID), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET)], [String(CommonConstants.OIDC_ISSUER_SESSIONS), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], [String(CommonConstants.OIDC_DELETE_CREDENTIAL_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], + [String(CommonConstants.OIDC_HOLDER_RESOLVE_OFFER), String(CommonConstants.URL_OIDC_HOLDER_RESOLVE_OFFER)], + [ + String(CommonConstants.OIDC_HOLDER_REQUEST_CREDENTIAL), + String(CommonConstants.URL_OIDC_HOLDER_REQUEST_CREDENTIAL) + ], + [ + String(CommonConstants.OIDC_HOLDER_RESOLVE_PROOF_REQUEST), + String(CommonConstants.URL_OIDC_HOLDER_RESOLVE_PROOF_REQUEST) + ], + [ + String(CommonConstants.OIDC_HOLDER_ACCEPT_PROOF_REQUEST), + String(CommonConstants.URL_OIDC_HOLDER_ACCEPT_PROOF_REQUEST) + ], [String(CommonConstants.X509_CREATE_CERTIFICATE), String(CommonConstants.URL_CREATE_X509_CERTIFICATE)], [String(CommonConstants.X509_DECODE_CERTIFICATE), String(CommonConstants.URL_DECODE_X509_CERTIFICATE)], [String(CommonConstants.X509_IMPORT_CERTIFICATE), String(CommonConstants.URL_IMPORT_X509_CERTIFICATE)], From 2e05268d3fd7737fe08bac19bd43729a59139fdb Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Mon, 15 Jun 2026 08:44:42 +0530 Subject: [PATCH 12/15] feat: replace context array with schemaUrl in SdJwtTemplate Signed-off-by: Sagar Khole --- .../dtos/oid4vc-issuer-template.dto.ts | 8 ++-- .../interfaces/oid4vc-template.interfaces.ts | 1 + .../src/oid4vc-issuance.service.ts | 41 +++++++++++-------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts index 7649c002c..784830047 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts @@ -197,12 +197,12 @@ export class SdJwtTemplateDto { vct: string; @ApiPropertyOptional({ - example: ['https://www.w3.org/2018/credentials/v1', 'https://json-ld.org/contexts/person.jsonld'], - description: 'JSON-LD context for the credential (optional, defaults to person schema for JSON-LD format)' + example: 'https://dev-schema.sovio.id/schemas/a99d55b6-c663-4af8-bcd3-4fe6699df91a', + description: 'JSON-LD schema URL for the credential' }) - @IsArray() + @IsString() @IsOptional() - context?: string[]; + schemaUrl?: string; @ApiProperty({ type: 'array', diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts index f1b235de2..9b7d72de0 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -2,6 +2,7 @@ import { Prisma, SignerOption } from '@prisma/client'; import { AttributeType, CredentialFormat } from '@credebl/enum/enum'; export interface SdJwtTemplate { vct: string; + schemaUrl?: string; context?: string[]; attributes: CredentialAttribute[]; } diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 86111e550..d8d7d0caf 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -94,26 +94,14 @@ export class Oid4vcIssuanceService { template: any, orgId: string ): Promise { - if ( - format === CredentialFormat.JwtVcJsonLd && - template && - 'context' in template && - Array.isArray(template.context) - ) { - const contexts = template.context; + if (format === CredentialFormat.JwtVcJsonLd && template) { let internalSchemaId: string | undefined; - for (const url of contexts) { - if (url === CREDENTIALS_CONTEXT_V1_URL || url === CREDENTIALS_CONTEXT_V2_URL) { - continue; - } - if (url.includes('/schemas/')) { - const parts = url.split('/'); - const potentialUuid = parts[parts.length - 1]; - if (uuidRegex.test(potentialUuid)) { - internalSchemaId = potentialUuid; - break; - } + if ('schemaUrl' in template && template.schemaUrl) { + const parts = template.schemaUrl.split('/'); + const potentialUuid = parts[parts.length - 1]; + if (uuidRegex.test(potentialUuid)) { + internalSchemaId = potentialUuid; } } @@ -400,6 +388,14 @@ export class Oid4vcIssuanceService { //TODO: add revert mechanism if agent call fails const { name, description, format, canBeRevoked, appearance, signerOption, noticeUrl } = credentialTemplate; + if ( + format === CredentialFormat.JwtVcJsonLd && + 'schemaUrl' in credentialTemplate.template && + credentialTemplate.template.schemaUrl + ) { + credentialTemplate.template.context = [CREDENTIALS_CONTEXT_V1_URL, credentialTemplate.template.schemaUrl]; + } + await this.validateTemplateAgainstSchema(format, credentialTemplate.template, orgId); const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer(name, issuerId); @@ -487,6 +483,15 @@ export class Oid4vcIssuanceService { }; const { name, description, format, canBeRevoked, appearance, signerOption, noticeUrl } = normalized; + if ( + normalized.template && + (format || template.format) === CredentialFormat.JwtVcJsonLd && + 'schemaUrl' in normalized.template && + normalized.template.schemaUrl + ) { + normalized.template.context = [CREDENTIALS_CONTEXT_V1_URL, normalized.template.schemaUrl]; + } + if (normalized.template && (format || template.format)) { await this.validateTemplateAgainstSchema(format || template.format, normalized.template, orgId); } From ae7e8ea8a1ca6c29bc9fb7964e576014323f37f0 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Tue, 23 Jun 2026 06:10:28 +0530 Subject: [PATCH 13/15] refactor(oid4vc): remove oid4vc-holder microservice and gateway integration Signed-off-by: Sagar Khole --- apps/api-gateway/src/app.module.ts | 2 - .../oid4vc-holder/dtos/oid4vc-holder.dto.ts | 40 ------ .../oid4vc-holder/oid4vc-holder.controller.ts | 128 ------------------ .../src/oid4vc-holder/oid4vc-holder.module.ts | 28 ---- .../oid4vc-holder/oid4vc-holder.service.ts | 40 ------ .../src/interfaces/oid4vc-holder.interface.ts | 17 --- apps/oid4vc-holder/src/main.ts | 27 ---- .../src/oid4vc-holder.controller.ts | 46 ------- .../oid4vc-holder/src/oid4vc-holder.module.ts | 38 ------ .../src/oid4vc-holder.service.ts | 102 -------------- libs/common/src/common.constant.ts | 13 -- libs/common/src/common.utils.ts | 13 -- 12 files changed, 494 deletions(-) delete mode 100644 apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts delete mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts delete mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts delete mode 100644 apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts delete mode 100644 apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts delete mode 100644 apps/oid4vc-holder/src/main.ts delete mode 100644 apps/oid4vc-holder/src/oid4vc-holder.controller.ts delete mode 100644 apps/oid4vc-holder/src/oid4vc-holder.module.ts delete mode 100644 apps/oid4vc-holder/src/oid4vc-holder.service.ts diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 6d94a7cda..3200a9219 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -24,7 +24,6 @@ import { IssuanceModule } from './issuance/issuance.module'; import { LoggerModule } from '@credebl/logger/logger.module'; import { NotificationModule } from './notification/notification.module'; import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; -import { Oid4vcHolderModule } from './oid4vc-holder/oid4vc-holder.module'; import { Oid4vpModule } from './oid4vc-verification/oid4vc-verification.module'; import { OrganizationModule } from './organization/organization.module'; @@ -79,7 +78,6 @@ import { shouldLoadOidcModules } from '@credebl/common/common.utils'; GeoLocationModule, CloudWalletModule, ConditionalModule.registerWhen(Oid4vcIssuanceModule, shouldLoadOidcModules), - ConditionalModule.registerWhen(Oid4vcHolderModule, shouldLoadOidcModules), ConditionalModule.registerWhen(Oid4vpModule, shouldLoadOidcModules), ConditionalModule.registerWhen(X509Module, shouldLoadOidcModules), KeycloakConfigModule diff --git a/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts b/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts deleted file mode 100644 index 8ca580a57..000000000 --- a/apps/api-gateway/src/oid4vc-holder/dtos/oid4vc-holder.dto.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; - -export class OidcResolveCredentialOfferDto { - @ApiProperty({ example: 'openid-credential-offer://?credential_offer_uri=...' }) - @IsString() - @IsNotEmpty() - credentialOfferUri: string; -} - -export class OidcRequestCredentialDto { - @ApiProperty({ example: 'openid-credential-offer://?credential_offer_uri=...' }) - @IsString() - @IsNotEmpty() - credentialOfferUri: string; - - @ApiProperty({ example: ['UniversityDegree'] }) - @IsArray() - @IsNotEmpty() - credentialsToRequest: string[]; - - @ApiPropertyOptional({ example: '1234' }) - @IsString() - @IsOptional() - txCode?: string; -} - -export class OidcResolveProofRequestDto { - @ApiProperty({ example: 'openid-vc-request://?request_uri=...' }) - @IsString() - @IsNotEmpty() - proofRequestUri: string; -} - -export class OidcAcceptProofRequestDto { - @ApiProperty({ example: 'openid-vc-request://?request_uri=...' }) - @IsString() - @IsNotEmpty() - proofRequestUri: string; -} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts deleted file mode 100644 index 1c22355b4..000000000 --- a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.controller.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Controller, Post, Body, UseGuards, HttpStatus, Res, Param, UseFilters, Logger } from '@nestjs/common'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiBearerAuth, - ApiForbiddenResponse, - ApiUnauthorizedResponse -} from '@nestjs/swagger'; -import { AuthGuard } from '@nestjs/passport'; -import { ApiResponseDto } from '../dtos/apiResponse.dto'; -import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; -import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; -import { Response } from 'express'; -import { IResponse } from '@credebl/common/interfaces/response.interface'; -import { Roles } from '../authz/decorators/roles.decorator'; -import { OrgRoles } from 'libs/org-roles/enums'; -import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; -import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; -import { Oid4vcHolderService } from './oid4vc-holder.service'; -import { - OidcAcceptProofRequestDto, - OidcRequestCredentialDto, - OidcResolveCredentialOfferDto, - OidcResolveProofRequestDto -} from './dtos/oid4vc-holder.dto'; - -@Controller('orgs/:orgId/oid4vc/holder') -@UseFilters(CustomExceptionFilter) -@ApiTags('OID4VC-Holder') -@ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) -@ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) -export class Oid4vcHolderController { - private readonly logger = new Logger('Oid4vcHolderController'); - constructor(private readonly oid4vcHolderService: Oid4vcHolderService) {} - - @Post('resolve-credential-offer') - @ApiOperation({ - summary: 'Resolve OID4VC Credential Offer', - description: 'Resolves an OID4VC credential offer for the specified organization.' - }) - @ApiResponse({ status: HttpStatus.OK, description: 'Credential offer resolved successfully.', type: ApiResponseDto }) - @ApiBearerAuth() - @Roles(OrgRoles.OWNER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - async oidcHolderResolveCredentialOffer( - @Param('orgId') orgId: string, - @Body() resolveDto: OidcResolveCredentialOfferDto, - @Res() res: Response - ): Promise { - const data = await this.oid4vcHolderService.oidcHolderResolveCredentialOffer(orgId, resolveDto); - const finalResponse: IResponse = { - statusCode: HttpStatus.OK, - message: 'Credential offer resolved successfully.', - data - }; - return res.status(HttpStatus.OK).json(finalResponse); - } - - @Post('request-credential') - @ApiOperation({ - summary: 'Request OID4VC Credential', - description: 'Requests and stores an OID4VC credential for the specified organization.' - }) - @ApiResponse({ status: HttpStatus.OK, description: 'Credential requested successfully.', type: ApiResponseDto }) - @ApiBearerAuth() - @Roles(OrgRoles.OWNER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - async oidcHolderRequestCredential( - @Param('orgId') orgId: string, - @Body() requestDto: OidcRequestCredentialDto, - @Res() res: Response - ): Promise { - const data = await this.oid4vcHolderService.oidcHolderRequestCredential(orgId, requestDto); - const finalResponse: IResponse = { - statusCode: HttpStatus.OK, - message: 'Credential requested successfully.', - data - }; - return res.status(HttpStatus.OK).json(finalResponse); - } - - @Post('resolve-proof-request') - @ApiOperation({ - summary: 'Resolve OID4VC Proof Request', - description: 'Resolves an OID4VC proof request for the specified organization.' - }) - @ApiResponse({ status: HttpStatus.OK, description: 'Proof request resolved successfully.', type: ApiResponseDto }) - @ApiBearerAuth() - @Roles(OrgRoles.OWNER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - async oidcHolderResolveProofRequest( - @Param('orgId') orgId: string, - @Body() resolveDto: OidcResolveProofRequestDto, - @Res() res: Response - ): Promise { - const data = await this.oid4vcHolderService.oidcHolderResolveProofRequest(orgId, resolveDto); - const finalResponse: IResponse = { - statusCode: HttpStatus.OK, - message: 'Proof request resolved successfully.', - data - }; - return res.status(HttpStatus.OK).json(finalResponse); - } - - @Post('accept-proof-request') - @ApiOperation({ - summary: 'Accept OID4VC Proof Request', - description: 'Accepts an OID4VC proof request for the specified organization.' - }) - @ApiResponse({ status: HttpStatus.OK, description: 'Proof request accepted successfully.', type: ApiResponseDto }) - @ApiBearerAuth() - @Roles(OrgRoles.OWNER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - async oidcHolderAcceptProofRequest( - @Param('orgId') orgId: string, - @Body() acceptDto: OidcAcceptProofRequestDto, - @Res() res: Response - ): Promise { - const data = await this.oid4vcHolderService.oidcHolderAcceptProofRequest(orgId, acceptDto); - const finalResponse: IResponse = { - statusCode: HttpStatus.OK, - message: 'Proof request accepted successfully.', - data - }; - return res.status(HttpStatus.OK).json(finalResponse); - } -} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts deleted file mode 100644 index 5cb87b1ee..000000000 --- a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Module } from '@nestjs/common'; -import { NATSClient } from '@credebl/common/NATSClient'; -import { getNatsOptions } from '@credebl/common/nats.config'; -import { CommonConstants } from '@credebl/common/common.constant'; -import { ClientsModule, Transport } from '@nestjs/microservices'; -import { HttpModule } from '@nestjs/axios'; -import { Oid4vcHolderController } from './oid4vc-holder.controller'; -import { Oid4vcHolderService } from './oid4vc-holder.service'; - -@Module({ - imports: [ - HttpModule, - ClientsModule.register([ - { - name: 'NATS_CLIENT', - transport: Transport.NATS, - options: getNatsOptions( - CommonConstants.OIDC4VC_HOLDER_SERVICE, - process.env.API_GATEWAY_NKEY_SEED, - process.env.NATS_CREDS_FILE - ) - } - ]) - ], - controllers: [Oid4vcHolderController], - providers: [Oid4vcHolderService, NATSClient] -}) -export class Oid4vcHolderModule {} diff --git a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts b/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts deleted file mode 100644 index bdd976488..000000000 --- a/apps/api-gateway/src/oid4vc-holder/oid4vc-holder.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NATSClient } from '@credebl/common/NATSClient'; -import { Inject, Injectable } from '@nestjs/common'; -import { ClientProxy } from '@nestjs/microservices'; -import { BaseService } from 'libs/service/base.service'; -import { - OidcAcceptProofRequestDto, - OidcRequestCredentialDto, - OidcResolveCredentialOfferDto, - OidcResolveProofRequestDto -} from './dtos/oid4vc-holder.dto'; - -@Injectable() -export class Oid4vcHolderService extends BaseService { - constructor( - @Inject('NATS_CLIENT') private readonly holderProxy: ClientProxy, - private readonly natsClient: NATSClient - ) { - super('Oid4vcHolderService'); - } - - async oidcHolderResolveCredentialOffer(orgId: string, holderPayload: OidcResolveCredentialOfferDto): Promise { - const payload = { orgId, holderPayload }; - return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-resolve-credential-offer', payload); - } - - async oidcHolderRequestCredential(orgId: string, holderPayload: OidcRequestCredentialDto): Promise { - const payload = { orgId, holderPayload }; - return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-request-credential', payload); - } - - async oidcHolderResolveProofRequest(orgId: string, holderPayload: OidcResolveProofRequestDto): Promise { - const payload = { orgId, holderPayload }; - return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-resolve-proof-request', payload); - } - - async oidcHolderAcceptProofRequest(orgId: string, holderPayload: OidcAcceptProofRequestDto): Promise { - const payload = { orgId, holderPayload }; - return this.natsClient.sendNatsMessage(this.holderProxy, 'oid4vc-holder-accept-proof-request', payload); - } -} diff --git a/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts b/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts deleted file mode 100644 index fc2f0e48b..000000000 --- a/apps/oid4vc-holder/src/interfaces/oid4vc-holder.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface IOidcHolderResolveCredentialOffer { - credentialOfferUri: string; -} - -export interface IOidcHolderRequestCredential { - credentialOfferUri: string; - credentialsToRequest: string[]; - txCode?: string; -} - -export interface IOidcHolderResolveProofRequest { - proofRequestUri: string; -} - -export interface IOidcHolderAcceptProofRequest { - proofRequestUri: string; -} diff --git a/apps/oid4vc-holder/src/main.ts b/apps/oid4vc-holder/src/main.ts deleted file mode 100644 index 5e9310dd1..000000000 --- a/apps/oid4vc-holder/src/main.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { HttpExceptionFilter } from 'libs/http-exception.filter'; -import { Logger } from '@nestjs/common'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; -import { CommonConstants } from '@credebl/common/common.constant'; -import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { Oid4vcHolderModule } from './oid4vc-holder.module'; - -const logger = new Logger(); - -async function bootstrap(): Promise { - const app = await NestFactory.createMicroservice(Oid4vcHolderModule, { - transport: Transport.NATS, - options: getNatsOptions( - CommonConstants.OIDC4VC_HOLDER_SERVICE, - process.env.OIDC4VC_HOLDER_NKEY_SEED, - process.env.NATS_CREDS_FILE - ) - }); - app.useLogger(app.get(NestjsLoggerServiceAdapter)); - app.useGlobalFilters(new HttpExceptionFilter()); - - await app.listen(); - logger.log('OID4VC-Holder-Service Microservice is listening to NATS '); -} -bootstrap(); diff --git a/apps/oid4vc-holder/src/oid4vc-holder.controller.ts b/apps/oid4vc-holder/src/oid4vc-holder.controller.ts deleted file mode 100644 index 802a20133..000000000 --- a/apps/oid4vc-holder/src/oid4vc-holder.controller.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Controller } from '@nestjs/common'; -import { MessagePattern } from '@nestjs/microservices'; -import { Oid4vcHolderService } from './oid4vc-holder.service'; -import { - IOidcHolderAcceptProofRequest, - IOidcHolderRequestCredential, - IOidcHolderResolveCredentialOffer, - IOidcHolderResolveProofRequest -} from './interfaces/oid4vc-holder.interface'; - -@Controller() -export class Oid4vcHolderController { - constructor(private readonly oid4vcHolderService: Oid4vcHolderService) {} - - @MessagePattern({ cmd: 'oid4vc-holder-resolve-credential-offer' }) - async oidcHolderResolveCredentialOffer(payload: { - orgId: string; - holderPayload: IOidcHolderResolveCredentialOffer; - }): Promise { - return this.oid4vcHolderService.oidcHolderResolveCredentialOffer(payload.orgId, payload.holderPayload); - } - - @MessagePattern({ cmd: 'oid4vc-holder-request-credential' }) - async oidcHolderRequestCredential(payload: { - orgId: string; - holderPayload: IOidcHolderRequestCredential; - }): Promise { - return this.oid4vcHolderService.oidcHolderRequestCredential(payload.orgId, payload.holderPayload); - } - - @MessagePattern({ cmd: 'oid4vc-holder-resolve-proof-request' }) - async oidcHolderResolveProofRequest(payload: { - orgId: string; - holderPayload: IOidcHolderResolveProofRequest; - }): Promise { - return this.oid4vcHolderService.oidcHolderResolveProofRequest(payload.orgId, payload.holderPayload); - } - - @MessagePattern({ cmd: 'oid4vc-holder-accept-proof-request' }) - async oidcHolderAcceptProofRequest(payload: { - orgId: string; - holderPayload: IOidcHolderAcceptProofRequest; - }): Promise { - return this.oid4vcHolderService.oidcHolderAcceptProofRequest(payload.orgId, payload.holderPayload); - } -} diff --git a/apps/oid4vc-holder/src/oid4vc-holder.module.ts b/apps/oid4vc-holder/src/oid4vc-holder.module.ts deleted file mode 100644 index 0d89112b7..000000000 --- a/apps/oid4vc-holder/src/oid4vc-holder.module.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Module, Logger } from '@nestjs/common'; -import { Oid4vcHolderController } from './oid4vc-holder.controller'; -import { Oid4vcHolderService } from './oid4vc-holder.service'; -import { CommonModule } from '@credebl/common'; -import { CommonConstants } from '@credebl/common/common.constant'; -import { LoggerModule } from '@credebl/logger'; -import { PrismaServiceModule } from '@credebl/prisma-service'; -import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; -import { GlobalConfigModule } from '@credebl/config'; -import { ContextInterceptorModule } from '@credebl/context'; -import { ClientsModule, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; -import { NATSClient } from '@credebl/common/NATSClient'; - -@Module({ - imports: [ - CommonModule, - LoggerModule, - PrismaServiceModule, - PlatformConfig, - GlobalConfigModule, - ContextInterceptorModule, - ClientsModule.register([ - { - name: 'NATS_CLIENT', - transport: Transport.NATS, - options: getNatsOptions( - CommonConstants.OIDC4VC_HOLDER_SERVICE, - process.env.OIDC4VC_HOLDER_NKEY_SEED, - process.env.NATS_CREDS_FILE - ) - } - ]) - ], - controllers: [Oid4vcHolderController], - providers: [Oid4vcHolderService, NATSClient, Logger] -}) -export class Oid4vcHolderModule {} diff --git a/apps/oid4vc-holder/src/oid4vc-holder.service.ts b/apps/oid4vc-holder/src/oid4vc-holder.service.ts deleted file mode 100644 index c5c156df5..000000000 --- a/apps/oid4vc-holder/src/oid4vc-holder.service.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Injectable, Logger, Inject } from '@nestjs/common'; -import { CommonService } from '@credebl/common'; -import { CommonConstants } from '@credebl/common/common.constant'; -import { PrismaService } from '@credebl/prisma-service'; -import { ClientProxy } from '@nestjs/microservices'; -import { NATSClient } from '@credebl/common/NATSClient'; -import { getAgentUrl } from '@credebl/common/common.utils'; -import { - IOidcHolderAcceptProofRequest, - IOidcHolderRequestCredential, - IOidcHolderResolveCredentialOffer, - IOidcHolderResolveProofRequest -} from './interfaces/oid4vc-holder.interface'; - -@Injectable() -export class Oid4vcHolderService { - private readonly logger = new Logger('Oid4vcHolderService'); - - constructor( - private readonly commonService: CommonService, - private readonly prisma: PrismaService, - @Inject('NATS_CLIENT') private readonly natsClientProxy: ClientProxy, - private readonly natsClient: NATSClient - ) {} - - private async getOrgAgentApiKey(orgId: string): Promise { - const agentDetails = await this.prisma.org_agents.findFirst({ - where: { orgId } - }); - if (!agentDetails || !agentDetails.apiKey) { - throw new Error(`Agent API key not found for org: ${orgId}`); - } - const decryptedToken = await this.commonService.decryptPassword(agentDetails.apiKey); - return decryptedToken; - } - - async _getAgentEndpoint(orgId: string): Promise { - const payload = { orgId }; - const agentDetails = await this.natsClient.sendNatsMessage( - this.natsClientProxy, - 'get-agent-details-by-org-id', - payload - ); - return (agentDetails as { agentEndPoint: string }).agentEndPoint; - } - - async oidcHolderResolveCredentialOffer(orgId: string, payload: IOidcHolderResolveCredentialOffer): Promise { - try { - const getApiKey = await this.getOrgAgentApiKey(orgId); - const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_RESOLVE_OFFER); - const data = await this.commonService - .httpPost(url, payload, { headers: { authorization: getApiKey } }) - .then(async (response) => response); - return data; - } catch (error) { - this.logger.error(`Error in oidcHolderResolveCredentialOffer in holder service : ${JSON.stringify(error)}`); - throw error; - } - } - - async oidcHolderRequestCredential(orgId: string, payload: IOidcHolderRequestCredential): Promise { - try { - const getApiKey = await this.getOrgAgentApiKey(orgId); - const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_REQUEST_CREDENTIAL); - const data = await this.commonService - .httpPost(url, payload, { headers: { authorization: getApiKey } }) - .then(async (response) => response); - return data; - } catch (error) { - this.logger.error(`Error in oidcHolderRequestCredential in holder service : ${JSON.stringify(error)}`); - throw error; - } - } - - async oidcHolderResolveProofRequest(orgId: string, payload: IOidcHolderResolveProofRequest): Promise { - try { - const getApiKey = await this.getOrgAgentApiKey(orgId); - const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_RESOLVE_PROOF_REQUEST); - const data = await this.commonService - .httpPost(url, payload, { headers: { authorization: getApiKey } }) - .then(async (response) => response); - return data; - } catch (error) { - this.logger.error(`Error in oidcHolderResolveProofRequest in holder service : ${JSON.stringify(error)}`); - throw error; - } - } - - async oidcHolderAcceptProofRequest(orgId: string, payload: IOidcHolderAcceptProofRequest): Promise { - try { - const getApiKey = await this.getOrgAgentApiKey(orgId); - const url = getAgentUrl(await this._getAgentEndpoint(orgId), CommonConstants.OIDC_HOLDER_ACCEPT_PROOF_REQUEST); - const data = await this.commonService - .httpPost(url, payload, { headers: { authorization: getApiKey } }) - .then(async (response) => response); - return data; - } catch (error) { - this.logger.error(`Error in oidcHolderAcceptProofRequest in holder service : ${JSON.stringify(error)}`); - throw error; - } - } -} diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 12a4be7b7..99d23bb4d 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -137,12 +137,6 @@ export enum CommonConstants { URL_OID4VP_VERIFICATION_SESSION = '/openid4vc/verification-sessions/create-presentation-request', URL_OIDC_VERIFIER_SESSION_AUTH_RESPONSE_VERIFY = '/openid4vc/verification-sessions/verify-authorization-response', - // OID4VC Holder URLs - URL_OIDC_HOLDER_RESOLVE_OFFER = '/openid4vc/holder/resolve-credential-offer', - URL_OIDC_HOLDER_REQUEST_CREDENTIAL = '/openid4vc/holder/request-credential', - URL_OIDC_HOLDER_RESOLVE_PROOF_REQUEST = '/openid4vc/holder/resolve-proof-request', - URL_OIDC_HOLDER_ACCEPT_PROOF_REQUEST = '/openid4vc/holder/accept-proof-request', - //X509 agent API URLs URL_CREATE_X509_CERTIFICATE = '/x509', URL_IMPORT_X509_CERTIFICATE = '/x509/import', @@ -403,7 +397,6 @@ export enum CommonConstants { CLOUD_WALLET_SERVICE = 'cloud-wallet', OIDC4VC_ISSUANCE_SERVICE = 'oid4vc-issuance', OIDC4VC_VERIFICATION_SERVICE = 'oid4vc-verification', - OIDC4VC_HOLDER_SERVICE = 'oid4vc-holder', OID4VP_VERIFICATION_SESSION = 'oid4vp-verification-session', X509_SERVICE = 'x509-service', @@ -441,12 +434,6 @@ export enum CommonConstants { OIDC_ISSUER_SESSIONS = 'get-oid4vc-sessions', OIDC_DELETE_CREDENTIAL_OFFER = 'delete-oid4vc-credential-offer', - // OID4VC Holder - OIDC_HOLDER_RESOLVE_OFFER = 'resolve-credential-offer', - OIDC_HOLDER_REQUEST_CREDENTIAL = 'request-credential', - OIDC_HOLDER_RESOLVE_PROOF_REQUEST = 'resolve-proof-request', - OIDC_HOLDER_ACCEPT_PROOF_REQUEST = 'accept-proof-request', - // OID4VP OIDC_VERIFIER_CREATE = 'create-oid4vp-verifier', OIDC_VERIFIER_UPDATE = 'update-oid4vp-verifier', diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index 708b21074..03e646738 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -109,19 +109,6 @@ export const getAgentUrl = (agentEndPoint: string, urlFlag: string, paramId?: st [String(CommonConstants.OIDC_ISSUER_SESSIONS_BY_ID), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET)], [String(CommonConstants.OIDC_ISSUER_SESSIONS), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], [String(CommonConstants.OIDC_DELETE_CREDENTIAL_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], - [String(CommonConstants.OIDC_HOLDER_RESOLVE_OFFER), String(CommonConstants.URL_OIDC_HOLDER_RESOLVE_OFFER)], - [ - String(CommonConstants.OIDC_HOLDER_REQUEST_CREDENTIAL), - String(CommonConstants.URL_OIDC_HOLDER_REQUEST_CREDENTIAL) - ], - [ - String(CommonConstants.OIDC_HOLDER_RESOLVE_PROOF_REQUEST), - String(CommonConstants.URL_OIDC_HOLDER_RESOLVE_PROOF_REQUEST) - ], - [ - String(CommonConstants.OIDC_HOLDER_ACCEPT_PROOF_REQUEST), - String(CommonConstants.URL_OIDC_HOLDER_ACCEPT_PROOF_REQUEST) - ], [String(CommonConstants.X509_CREATE_CERTIFICATE), String(CommonConstants.URL_CREATE_X509_CERTIFICATE)], [String(CommonConstants.X509_DECODE_CERTIFICATE), String(CommonConstants.URL_DECODE_X509_CERTIFICATE)], [String(CommonConstants.X509_IMPORT_CERTIFICATE), String(CommonConstants.URL_IMPORT_X509_CERTIFICATE)], From 3a70c87c03c3e9b794dfb7c55ff2a200ddd803f5 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Tue, 23 Jun 2026 19:07:56 +0530 Subject: [PATCH 14/15] refactor(oid4vc): resolve security warnings, harden schema validation, and fix linting Signed-off-by: Sagar Khole --- .../helpers/credential-sessions.builder.ts | 26 +++++++++++++---- .../src/oid4vc-issuance.service.ts | 28 +++++++++++++++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index bf8d1b6a0..a8f3c167b 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -539,17 +539,33 @@ function buildJwtVcJsonLdCredential( const vct = (templateRecord.attributes as any)?.vct; let typeName = ''; if ('string' === typeof vct) { - const lastSlash = vct.lastIndexOf('/'); - typeName = -1 !== lastSlash ? vct.substring(lastSlash + 1) : vct; + const cleanVct = vct.replace(/\/+$/, ''); + const lastSlash = cleanVct.lastIndexOf('/'); + typeName = -1 !== lastSlash ? cleanVct.substring(lastSlash + 1) : cleanVct; } else { typeName = templateRecord.name.replace(/\s+/g, ''); } const templateContext = (templateRecord.attributes as any)?.context; - const context = Array.isArray(templateContext) ? templateContext : ['https://www.w3.org/2018/credentials/v1']; + const context = Array.isArray(templateContext) ? [...templateContext] : ['https://www.w3.org/2018/credentials/v1']; + const requiredContextUrl = 'https://www.w3.org/2018/credentials/v1'; + const normalizeUrlForComparison = (value: unknown): string | null => { + if ('string' !== typeof value) { + return null; + } + try { + return new URL(value).toString(); + } catch { + return null; + } + }; + const normalizedRequiredContext = normalizeUrlForComparison(requiredContextUrl); + const hasRequiredContext = context.some( + (ctx) => null !== normalizedRequiredContext && normalizeUrlForComparison(ctx) === normalizedRequiredContext + ); - if (!context.includes('https://www.w3.org/2018/credentials/v1')) { - context.unshift('https://www.w3.org/2018/credentials/v1'); + if (!hasRequiredContext) { + context.unshift(requiredContextUrl); } const wrappedPayload: Record = { diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index d8d7d0caf..35a3fb35b 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -98,10 +98,28 @@ export class Oid4vcIssuanceService { let internalSchemaId: string | undefined; if ('schemaUrl' in template && template.schemaUrl) { - const parts = template.schemaUrl.split('/'); - const potentialUuid = parts[parts.length - 1]; - if (uuidRegex.test(potentialUuid)) { - internalSchemaId = potentialUuid; + let isInternal = false; + try { + const schemaUrlObj = new URL(template.schemaUrl); + const serverUrlStr = process.env.SCHEMA_FILE_SERVER_URL; + if (serverUrlStr) { + const serverUrlObj = new URL(serverUrlStr); + if (schemaUrlObj.hostname === serverUrlObj.hostname) { + isInternal = true; + } + } + } catch (error) { + if (process.env.SCHEMA_FILE_SERVER_URL && template.schemaUrl.startsWith(process.env.SCHEMA_FILE_SERVER_URL)) { + isInternal = true; + } + } + + if (isInternal) { + const parts = template.schemaUrl.split('/'); + const potentialUuid = parts[parts.length - 1]; + if (uuidRegex.test(potentialUuid)) { + internalSchemaId = potentialUuid; + } } } @@ -113,7 +131,7 @@ export class Oid4vcIssuanceService { .toPromise(); } catch (error) { this.logger.error(`Error fetching schema ${internalSchemaId} from ledger: ${error.message}`); - return; + throw new NotFoundException(`Schema with ID ${internalSchemaId} not found or unreachable: ${error.message}`); } if (schemaResponse && schemaResponse.attributes) { From f73b17911c1597bf5011d823eaa9e2bae59c6df9 Mon Sep 17 00:00:00 2001 From: Sagar Khole Date: Thu, 25 Jun 2026 19:19:19 +0530 Subject: [PATCH 15/15] feat(oid4vc):ldp_vc support for jsonLd Signed-off-by: Sagar Khole --- .../dtos/issuer-sessions.dto.ts | 6 +-- .../dtos/oid4vc-issuer-template.dto.ts | 12 +++-- .../oid4vc-issuer-sessions.interfaces.ts | 3 +- .../helpers/credential-sessions.builder.ts | 16 +++++-- .../libs/helpers/issuer.metadata.ts | 46 ++++++++++++++++++- .../src/oid4vc-issuance.service.ts | 7 +-- 6 files changed, 76 insertions(+), 14 deletions(-) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts index bb29994f2..ae64acd35 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts @@ -274,12 +274,12 @@ export class CredentialDto { @ApiProperty({ description: 'Credential format type', - enum: ['mso_mdoc', 'vc+sd-jwt', 'jwt_vc_json-ld'], + enum: ['mso_mdoc', 'vc+sd-jwt', 'jwt_vc_json-ld', 'ldp_vc'], example: 'mso_mdoc' }) @IsString() - @IsIn(['mso_mdoc', 'vc+sd-jwt', 'jwt_vc_json-ld'], { - message: 'format must be either "mso_mdoc", "vc+sd-jwt" or "jwt_vc_json-ld"' + @IsIn(['mso_mdoc', 'vc+sd-jwt', 'jwt_vc_json-ld', 'ldp_vc'], { + message: 'format must be either "mso_mdoc", "vc+sd-jwt", "jwt_vc_json-ld" or "ldp_vc"' }) format: string; diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts index 784830047..857b7b719 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts @@ -239,9 +239,11 @@ export class CreateCredentialTemplateDto { @ValidateIf( (o: CreateCredentialTemplateDto) => - CredentialFormat.SdJwtVc === o.format || CredentialFormat.JwtVcJsonLd === o.format + CredentialFormat.SdJwtVc === o.format || + CredentialFormat.JwtVcJsonLd === o.format || + CredentialFormat.LdpVc === o.format ) - @IsEmpty({ message: 'doctype must not be provided when format is "dc+sd-jwt" or "jwt_vc_json-ld"' }) + @IsEmpty({ message: 'doctype must not be provided when format is "dc+sd-jwt", "jwt_vc_json-ld" or "ldp_vc"' }) readonly _doctypeAbsentGuard?: unknown; @ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.Mdoc === o.format) @@ -257,7 +259,11 @@ export class CreateCredentialTemplateDto { @Type(({ object }) => { if (object.format === CredentialFormat.Mdoc) { return MdocTemplateDto; - } else if (object.format === CredentialFormat.SdJwtVc || object.format === CredentialFormat.JwtVcJsonLd) { + } else if ( + object.format === CredentialFormat.SdJwtVc || + object.format === CredentialFormat.JwtVcJsonLd || + object.format === CredentialFormat.LdpVc + ) { return SdJwtTemplateDto; } }) diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts index a9334383e..05561af4d 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts @@ -6,7 +6,8 @@ import { OpenId4VcIssuanceSessionState } from '@credebl/enum/enum'; export enum CredentialFormat { SdJwtVc = 'vc+sd-jwt', MsoMdoc = 'mso_mdoc', - JwtVcJsonLd = 'jwt_vc_json-ld' + JwtVcJsonLd = 'jwt_vc_json-ld', + LdpVc = 'ldp_vc' } export enum SignerMethodOption { diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index a8f3c167b..75f610c06 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -153,19 +153,25 @@ function mapDbFormatToApiFormat(dbFormat: string): CredentialFormat { if (['jwt_vc_json-ld', 'jwt-vc-json-ld', 'w3c-jwt-json-ld'].includes(normalized)) { return CredentialFormat.JwtVcJsonLd; } + if (['ldp_vc', 'ldp-vc'].includes(normalized)) { + return CredentialFormat.LdpVc; + } if ('mso_mdoc' === normalized || 'mso-mdoc' === normalized || 'mdoc' === normalized) { return CredentialFormat.Mdoc; } throw new UnprocessableEntityException(`Unsupported template format: ${dbFormat}`); } -function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' | 'jwt-vc-json-ld' { +function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' | 'jwt-vc-json-ld' | 'ldp-vc' { if (apiFormat === CredentialFormat.SdJwtVc) { return 'sdjwt'; } if (apiFormat === CredentialFormat.JwtVcJsonLd) { return 'jwt-vc-json-ld'; } + if (apiFormat === CredentialFormat.LdpVc) { + return 'ldp-vc'; + } return 'mdoc'; } @@ -222,7 +228,11 @@ export function validatePayloadAgainstTemplate(template: any, payload: any): { v } }; - if (CredentialFormat.SdJwtVc === template.format || CredentialFormat.JwtVcJsonLd === template.format) { + if ( + CredentialFormat.SdJwtVc === template.format || + CredentialFormat.JwtVcJsonLd === template.format || + CredentialFormat.LdpVc === template.format + ) { validateAttributes((template.attributes as SdJwtTemplate).attributes ?? [], payload); } else if (CredentialFormat.Mdoc === template.format) { const namespaces = payload?.namespaces; @@ -629,7 +639,7 @@ export function buildCredentialOfferPayload( if (apiFormat === CredentialFormat.SdJwtVc) { return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); } - if (apiFormat === CredentialFormat.JwtVcJsonLd) { + if (apiFormat === CredentialFormat.JwtVcJsonLd || apiFormat === CredentialFormat.LdpVc) { return buildJwtVcJsonLdCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); } if (apiFormat === CredentialFormat.Mdoc) { diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index f252330b5..d56d66df4 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -330,6 +330,46 @@ export function buildJwtVcJsonLdCredentialConfig( }; } +export function buildLdpVcCredentialConfig(name: string, template: SdJwtTemplate): Record { + const formatSuffix = 'ldp-vc'; + + // Determine the unique key for this credential configuration + const configKey = `${name}-${formatSuffix}`; + const credentialScope = `openid4vc:${template.vct}-${formatSuffix}`; + + const claims = buildClaimsFromTemplate(template); + + const { vct } = template; + let typeName = name ? name.replace(/[^a-zA-Z0-9]/g, '') : 'GenericCredential'; + if ('string' === typeof vct && '' !== vct.trim()) { + const lastSlash = vct.lastIndexOf('/'); + typeName = -1 !== lastSlash ? vct.substring(lastSlash + 1) : vct; + } + + return { + [configKey]: { + format: CredentialFormat.LdpVc, + scope: credentialScope, + vct: template.vct, + credential_signing_alg_values_supported: ['Ed25519Signature2018', 'Ed25519Signature2020', 'EdDSA'], + cryptographic_binding_methods_supported: ['did:key'], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: ['Ed25519Signature2018', 'Ed25519Signature2020', 'EdDSA'] + } + }, + credential_definition: { + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential', typeName] + }, + credential_metadata: { + claims, + display: [] + } + } + }; +} + //TODO: Fix this eslint issue // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function buildCredentialConfig( @@ -342,6 +382,8 @@ export function buildCredentialConfig( return buildSdJwtCredentialConfig(name, template as SdJwtTemplate); case CredentialFormat.JwtVcJsonLd: return buildJwtVcJsonLdCredentialConfig(name, template as SdJwtTemplate); + case CredentialFormat.LdpVc: + return buildLdpVcCredentialConfig(name, template as SdJwtTemplate); case CredentialFormat.Mdoc: return buildMdocCredentialConfig(name, template as MdocTemplate); default: @@ -367,7 +409,9 @@ export function buildCredentialConfigurationsSupported(templateRows: unknown[]): ? CredentialFormat.Mdoc : format === CredentialFormat.JwtVcJsonLd ? CredentialFormat.JwtVcJsonLd - : CredentialFormat.SdJwtVc + : format === CredentialFormat.LdpVc + ? CredentialFormat.LdpVc + : CredentialFormat.SdJwtVc ); const appearance = coerceJson(templateRow.appearance as Prisma.JsonValue); diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 35a3fb35b..1ba61e870 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -94,7 +94,7 @@ export class Oid4vcIssuanceService { template: any, orgId: string ): Promise { - if (format === CredentialFormat.JwtVcJsonLd && template) { + if ((format === CredentialFormat.JwtVcJsonLd || format === CredentialFormat.LdpVc) && template) { let internalSchemaId: string | undefined; if ('schemaUrl' in template && template.schemaUrl) { @@ -407,7 +407,7 @@ export class Oid4vcIssuanceService { const { name, description, format, canBeRevoked, appearance, signerOption, noticeUrl } = credentialTemplate; if ( - format === CredentialFormat.JwtVcJsonLd && + (format === CredentialFormat.JwtVcJsonLd || format === CredentialFormat.LdpVc) && 'schemaUrl' in credentialTemplate.template && credentialTemplate.template.schemaUrl ) { @@ -503,7 +503,8 @@ export class Oid4vcIssuanceService { if ( normalized.template && - (format || template.format) === CredentialFormat.JwtVcJsonLd && + ((format || template.format) === CredentialFormat.JwtVcJsonLd || + (format || template.format) === CredentialFormat.LdpVc) && 'schemaUrl' in normalized.template && normalized.template.schemaUrl ) {