From f52f1d228a3912711c191eefbae8f263a583bc2e Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Fri, 12 Sep 2025 18:46:25 +0530 Subject: [PATCH 01/43] feat: oidc implementation Signed-off-by: Tipu_Singh --- .../src/agent-service.controller.ts | 34 + .../src/agent-service.service.ts | 78 +++ .../src/issuance/dtos/issuer-sessions.dto.ts | 224 +++++++ .../issuance/dtos/oidc-issuer-template.dto.ts | 182 +++++ .../src/issuance/dtos/oidc-issuer.dto.ts | 289 ++++++++ .../src/issuance/issuance.module.ts | 3 +- .../src/issuance/issuance.service.ts | 91 ++- .../src/issuance/oidc.controller.ts | 490 ++++++++++++++ apps/issuance/constant/issuance.ts | 6 + .../interfaces/oidc-issuance.interfaces.ts | 147 +++++ .../oidc-issuer-sessions.interfaces.ts | 51 ++ .../interfaces/oidc-template.interface.ts | 26 + .../helpers/credential-sessions.builder.ts | 271 ++++++++ apps/issuance/libs/helpers/issuer.metadata.ts | 244 +++++++ apps/issuance/src/issuance.controller.ts | 130 +++- apps/issuance/src/issuance.module.ts | 2 + apps/issuance/src/issuance.repository.ts | 185 ++++++ apps/issuance/src/oidc-issuance.service.ts | 622 ++++++++++++++++++ libs/common/src/common.constant.ts | 17 +- libs/common/src/common.utils.ts | 11 +- libs/common/src/response-messages/index.ts | 48 ++ .../migration.sql | 94 +++ .../migration.sql | 17 + .../migration.sql | 32 + .../migration.sql | 21 + .../migration.sql | 6 + .../migration.sql | 18 + libs/prisma-service/prisma/schema.prisma | 37 ++ 28 files changed, 3370 insertions(+), 6 deletions(-) create mode 100644 apps/api-gateway/src/issuance/dtos/issuer-sessions.dto.ts create mode 100644 apps/api-gateway/src/issuance/dtos/oidc-issuer-template.dto.ts create mode 100644 apps/api-gateway/src/issuance/dtos/oidc-issuer.dto.ts create mode 100644 apps/api-gateway/src/issuance/oidc.controller.ts create mode 100644 apps/issuance/constant/issuance.ts create mode 100644 apps/issuance/interfaces/oidc-issuance.interfaces.ts create mode 100644 apps/issuance/interfaces/oidc-issuer-sessions.interfaces.ts create mode 100644 apps/issuance/interfaces/oidc-template.interface.ts create mode 100644 apps/issuance/libs/helpers/credential-sessions.builder.ts create mode 100644 apps/issuance/libs/helpers/issuer.metadata.ts create mode 100644 apps/issuance/src/oidc-issuance.service.ts create mode 100644 libs/prisma-service/prisma/migrations/20250814141522_add_supported_protocol/migration.sql create mode 100644 libs/prisma-service/prisma/migrations/20250822104325_oidc_issuer_templates/migration.sql create mode 100644 libs/prisma-service/prisma/migrations/20250822145801_fix_issuer_relation/migration.sql create mode 100644 libs/prisma-service/prisma/migrations/20250902082321_add_org_agent_id_to_oidc_issuer/migration.sql create mode 100644 libs/prisma-service/prisma/migrations/20250904181430_added_fields_in_oidc_issuer/migration.sql create mode 100644 libs/prisma-service/prisma/migrations/20250911102632_added_field_signer_option_in_credential_templates/migration.sql diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index d368781cd..dcd3cf149 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -323,4 +323,38 @@ export class AgentServiceController { async agentdetailsByOrgId(payload: { orgId: string }): Promise { return this.agentServiceService.getAgentDetails(payload.orgId); } + + @MessagePattern({ cmd: 'agent-create-oidc-issuer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcIssuerCreate(payload: { issuerCreation; url: string; orgId: string }): Promise { + return this.agentServiceService.oidcIssuerCreate(payload.issuerCreation, payload.url, payload.orgId); + } + @MessagePattern({ cmd: 'delete-oidc-issuer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcDeleteIssuer(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.deleteOidcIssuer(payload.url, payload.orgId); + } + @MessagePattern({ cmd: 'agent-create-oidc-template' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcIssuerTemplate(payload: { templatePayload; url: string; orgId: string }): Promise { + return this.agentServiceService.oidcIssuerTemplate(payload.templatePayload, payload.url, payload.orgId); + } + //TODO: change message for oidc + @MessagePattern({ cmd: 'oidc-get-issuer-by-id' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcGetIssuerById(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.oidcGetIssuerById(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'oidc-get-issuers' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcGetIssuers(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.oidcGetIssuers(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-service-oidc-create-credential-offer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcCreateCredentialOffer(payload: { credentialPayload; url: string; orgId: string }): Promise { + return this.agentServiceService.oidcCreateCredentialOffer(payload.credentialPayload, payload.url, payload.orgId); + } } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index fb41a1801..c7d32a30b 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -1412,6 +1412,84 @@ export class AgentServiceService { } } + async oidcIssuerCreate(issueData, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpPost(url, issueData, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcIssuerCreate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async deleteOidcIssuer(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpDelete(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in deleteOidcIssuer in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcGetIssuerById(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcGetIssuerById in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcGetIssuers(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcGetIssuers in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcCreateCredentialOffer(credentialPayload, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpPost(url, credentialPayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcCreateCredentialOffer in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcIssuerTemplate(templatePayload, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpPut(url, templatePayload, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in oidcIssuerTemplate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + async getIssueCredentials(url: string, apiKey: string): Promise { try { const data = await this.commonService diff --git a/apps/api-gateway/src/issuance/dtos/issuer-sessions.dto.ts b/apps/api-gateway/src/issuance/dtos/issuer-sessions.dto.ts new file mode 100644 index 000000000..a31c65799 --- /dev/null +++ b/apps/api-gateway/src/issuance/dtos/issuer-sessions.dto.ts @@ -0,0 +1,224 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ + +import { + IsArray, + IsEnum, + IsNotEmpty, + IsObject, + IsOptional, + IsString, + Matches, + ValidateNested, + registerDecorator, + ValidationOptions, + IsInt, + Min, + IsIn, + ArrayMinSize, + IsUrl, + ValidatorConstraint, + ValidatorConstraintInterface, + ValidationArguments, + Validate +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +export enum CredentialFormat { + SdJwtVc = 'vc+sd-jwt', + Mdoc = 'mdoc' +} + +export enum SignerMethodOption { + DID = 'did', + X5C = 'x5c' +} + +/** ---------- custom validator: disclosureFrame ---------- */ +function isDisclosureFrameValue(v: unknown): boolean { + if ('boolean' === typeof v) { + return true; + } + if (v && 'object' === typeof v && !Array.isArray(v)) { + return Object.values(v as Record).every((x) => 'boolean' === typeof x); + } + return false; +} + +export function IsDisclosureFrame(options?: ValidationOptions) { + return function (object: unknown, propertyName: string) { + registerDecorator({ + name: 'IsDisclosureFrame', + target: (object as object).constructor, + propertyName, + options, + validator: { + validate(value: unknown) { + if (value === undefined) { + return true; + } + if (!value || 'object' !== typeof value || Array.isArray(value)) { + return false; + } + return Object.values(value as Record).every(isDisclosureFrameValue); + }, + defaultMessage() { + return 'disclosureFrame must be a map of booleans or nested maps of booleans'; + } + } + }); + }; +} + +/** ---------- payload DTOs ---------- */ +export class CredentialPayloadDto { + @ApiPropertyOptional() + @IsOptional() + @IsString() + vct?: string; + + @ApiPropertyOptional({ example: 'Garry' }) + @IsOptional() + @IsString() + full_name?: string; + + @ApiPropertyOptional({ example: '2000-01-01', description: 'YYYY-MM-DD' }) + @IsOptional() + @Matches(/^\d{4}-\d{2}-\d{2}$/, { message: 'birth_date must be YYYY-MM-DD' }) + birth_date?: string; + + @ApiPropertyOptional({ example: 'Africa' }) + @IsOptional() + @IsString() + birth_place?: string; + + @ApiPropertyOptional({ example: 'James Bear' }) + @IsOptional() + @IsString() + parent_names?: string; + + [key: string]: unknown; +} + +export class CredentialRequestDto { + @ApiProperty({ example: '1b2d3c4e-...' }) + @IsString() + @IsNotEmpty() + templateId!: string; + + @ApiProperty({ enum: CredentialFormat, example: CredentialFormat.SdJwtVc }) + @IsEnum(CredentialFormat) + format!: CredentialFormat; + + @ApiProperty({ + type: CredentialPayloadDto, + description: 'Credential payload (structure depends on the format)' + }) + @ValidateNested() + @Type(() => CredentialPayloadDto) + payload!: CredentialPayloadDto; + + @ApiPropertyOptional({ + description: 'Selective disclosure frame (claim -> boolean or nested map).', + example: { full_name: true, birth_date: true, birth_place: false, parent_names: false }, + required: false + }) + @IsOptional() + @IsObject() + @IsDisclosureFrame() + disclosureFrame?: Record>; +} + +/** ---------- auth-config DTOs ---------- */ +export class TxCodeDto { + @ApiPropertyOptional({ example: 'test abc' }) + @IsOptional() + @IsString() + description?: string; + + @ApiProperty({ example: 4 }) + @IsInt() + @Min(1) + length!: number; + + @ApiProperty({ example: 'numeric', enum: ['numeric'] }) + @IsString() + @IsIn(['numeric']) + input_mode!: 'numeric'; +} + +export class PreAuthorizedCodeFlowConfigDto { + @ApiProperty({ type: TxCodeDto }) + @ValidateNested() + @Type(() => TxCodeDto) + txCode!: TxCodeDto; + + @ApiProperty({ + example: 'http://localhost:4001/oid4vci/abc-gov', + description: 'AS (Authorization Server) base URL' + }) + @IsUrl({ require_tld: false }) + authorizationServerUrl!: string; +} + +export class AuthorizationCodeFlowConfigDto { + @ApiProperty({ + example: 'https://id.credebl.ae:8443/realms/credebl', + description: 'AS (Authorization Server) base URL' + }) + @IsUrl({ require_tld: false }) + authorizationServerUrl!: string; +} + +/** ---------- class-level constraint: EXACTLY ONE of the two configs ---------- */ +@ValidatorConstraint({ name: 'ExactlyOneOf', async: false }) +class ExactlyOneOfConstraint implements ValidatorConstraintInterface { + validate(_: unknown, args: ValidationArguments) { + const obj = args.object as Record; + const keys = (args.constraints ?? []) as string[]; + const present = keys.filter((k) => obj[k] !== undefined && null !== obj[k]); + return 1 === present.length; + } + defaultMessage(args: ValidationArguments) { + const keys = (args.constraints ?? []) as string[]; + return `Exactly one of [${keys.join(', ')}] must be provided (not both, not neither).`; + } +} +function ExactlyOneOf(keys: string[], options?: ValidationOptions) { + return Validate(ExactlyOneOfConstraint, keys, options); +} + +/** ---------- root DTO (no authenticationType) ---------- */ +export class CreateOidcCredentialOfferDto { + @ApiProperty({ + type: [CredentialRequestDto], + description: 'At least one credential to be issued.' + }) + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => CredentialRequestDto) + credentials!: CredentialRequestDto[]; + + // Each is optional individually; XOR rule below enforces exactly one present. + @ApiPropertyOptional({ type: PreAuthorizedCodeFlowConfigDto }) + @IsOptional() + @ValidateNested() + @Type(() => PreAuthorizedCodeFlowConfigDto) + preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfigDto; + + @ApiPropertyOptional({ type: AuthorizationCodeFlowConfigDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorizationCodeFlowConfigDto) + authorizationCodeFlowConfig?: AuthorizationCodeFlowConfigDto; + + issuerId?: string; + + // Host the class-level XOR validator on a dummy property + @ExactlyOneOf(['preAuthorizedCodeFlowConfig', 'authorizationCodeFlowConfig'], { + message: 'Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.' + }) + private readonly _exactlyOne?: unknown; +} diff --git a/apps/api-gateway/src/issuance/dtos/oidc-issuer-template.dto.ts b/apps/api-gateway/src/issuance/dtos/oidc-issuer-template.dto.ts new file mode 100644 index 000000000..efd901a89 --- /dev/null +++ b/apps/api-gateway/src/issuance/dtos/oidc-issuer-template.dto.ts @@ -0,0 +1,182 @@ +/* eslint-disable camelcase */ +import { + IsString, + IsBoolean, + IsOptional, + IsEnum, + ValidateNested, + IsObject, + IsNotEmpty, + IsArray +} from 'class-validator'; +import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath, PartialType } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { DisplayDto } from './oidc-issuer.dto'; + +export class CredentialAttributeDto { + @ApiProperty({ required: false, description: 'Whether the attribute is mandatory' }) + @IsOptional() + @IsBoolean() + mandatory?: boolean; + + @ApiProperty({ description: 'Type of the attribute value (string, number, date, etc.)' }) + @IsString() + value_type: string; + + @ApiProperty({ type: [DisplayDto], required: false, description: 'Localized display values' }) + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => DisplayDto) + display?: DisplayDto[]; +} + +class LogoDto { + @ApiPropertyOptional({ + example: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg' + }) + @IsString() + @IsNotEmpty() + uri: string; + + @ApiPropertyOptional({ example: 'abc_logo' }) + @IsString() + @IsOptional() + alt_text?: string; +} + +class CredentialDisplayDto { + @ApiPropertyOptional({ example: 'Birth Certificate' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiPropertyOptional({ example: 'Official record of birth' }) + @IsString() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ example: 'en' }) + @IsString() + @IsOptional() + locale?: string; + + @ApiPropertyOptional({ + example: { + uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', + alt_text: 'abc_logo' + } + }) + @IsOptional() + @IsObject() + @ValidateNested() + @Type(() => LogoDto) + logo?: LogoDto; +} + +export class AppearanceDto { + @ApiPropertyOptional({ + example: [ + { + locale: 'en', + name: 'Birth Certificate', + description: 'Official record of birth', + logo: { + uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', + alt_text: 'abc_logo' + } + }, + { + locale: 'ar', + name: 'شهادة الميلاد', + description: 'سجل رسمي للولادة', + logo: { + uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', + alt_text: 'شعار abc' + } + } + ] + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CredentialDisplayDto) + display: CredentialDisplayDto[]; +} + +export enum SignerOption { + DID = 'did', + X509 = 'x509' +} +@ApiExtraModels(CredentialAttributeDto) +export class CreateCredentialTemplateDto { + @ApiProperty({ description: 'Template name' }) + @IsString() + name: string; + + @ApiProperty({ required: false, description: 'Optional description for the template' }) + @IsOptional() + @IsString() + description?: string; + + @ApiProperty({ + description: 'Signer option (did or x509)', + enum: SignerOption, + example: SignerOption.DID + }) + @IsEnum(SignerOption) + signerOption!: SignerOption; + + @ApiProperty({ enum: ['sd-jwt-vc', 'mdoc'], description: 'Credential format type' }) + @IsEnum(['sd-jwt-vc', 'mso_mdoc', 'vc+sd-jwt']) + format: 'sd-jwt-vc' | 'mso_mdoc ' | 'vc+sd-jwt'; + + @ApiProperty({ default: false, description: 'Indicates whether credentials can be revoked' }) + @IsBoolean() + canBeRevoked = false; + + @ApiProperty({ + type: 'object', + additionalProperties: { $ref: getSchemaPath(CredentialAttributeDto) }, + description: 'Attributes included in the credential template' + }) + @IsObject() + attributes: Record; + + @ApiProperty({ + type: Object, + required: false, + description: 'Appearance configuration for credentials (branding, colors, etc.)' + }) + @ApiPropertyOptional({ + type: AppearanceDto, + example: { + display: [ + { + locale: 'en', + name: 'Birth Certificate', + description: 'Official record of birth', + logo: { + uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', + alt_text: 'abc_logo' + } + }, + { + locale: 'ar', + name: 'شهادة الميلاد', + description: 'سجل رسمي للولادة', + logo: { + uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', + alt_text: 'شعار abc' + } + } + ] + } + }) + @IsOptional() + @ValidateNested() + @Type(() => AppearanceDto) + appearance?: AppearanceDto; + + issuerId: string; +} + +export class UpdateCredentialTemplateDto extends PartialType(CreateCredentialTemplateDto) {} diff --git a/apps/api-gateway/src/issuance/dtos/oidc-issuer.dto.ts b/apps/api-gateway/src/issuance/dtos/oidc-issuer.dto.ts new file mode 100644 index 000000000..89216fcb7 --- /dev/null +++ b/apps/api-gateway/src/issuance/dtos/oidc-issuer.dto.ts @@ -0,0 +1,289 @@ +/* eslint-disable camelcase */ +import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; +import { + IsString, + IsOptional, + IsBoolean, + IsArray, + ValidateNested, + IsObject, + IsUrl, + IsNotEmpty, + IsDefined, + IsInt +} from 'class-validator'; +import { plainToInstance, Transform, Type } from 'class-transformer'; + +export class ClaimDto { + @ApiProperty({ + description: 'The unique key for the claim (e.g. email, name)', + example: 'email' + }) + @IsString() + key: string; + + @ApiProperty({ + description: 'The display label for the claim', + example: 'Email Address' + }) + @IsString() + label: string; + + @ApiProperty({ + description: 'Whether this claim is required for issuance', + example: true + }) + @IsBoolean() + required: boolean; +} + +export class LogoDto { + @ApiProperty({ + description: 'URI pointing to the logo image', + example: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg' + }) + @IsUrl() + uri: string; + + @ApiProperty({ + description: 'Alternative text for the logo (for accessibility)', + example: 'ABC Company Logo' + }) + @IsString() + alt_text: string; +} + +export class DisplayDto { + @ApiProperty({ + description: 'The locale for display text', + example: 'en-US' + }) + @IsString() + locale: string; + + @ApiProperty({ + description: 'The display name for the credential/claim', + example: 'Student ID Card' + }) + @IsString({ message: 'Error from DisplayDto -> name' }) + name: string; + + @ApiPropertyOptional({ + description: 'A short description for the credential/claim', + example: 'Digital credential issued to enrolled students' + }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ + description: 'Logo display information for the issuer', + type: LogoDto + }) + @IsOptional() + @Type(() => LogoDto) + logo?: LogoDto; +} + +@ApiExtraModels(ClaimDto) +export class CredentialConfigurationDto { + @ApiProperty({ + description: 'The format of the credential', + example: 'jwt_vc_json' + }) + @IsString() + @IsDefined({ message: 'format field is required' }) + @IsNotEmpty({ message: 'format property is required' }) + format: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + vct?: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + doctype?: string; + + @ApiProperty() + @IsString() + scope: string; + + // @ApiProperty({ + // description: 'List of claims supported in this credential', + // type: [ClaimDto], + // }) + // @IsArray() + // @ValidateNested({ each: true }) + // @Type(() => ClaimDto) + // claims: ClaimDto[] + @ApiProperty({ + description: 'Claims supported by this credential', + type: 'object', + additionalProperties: { $ref: getSchemaPath(ClaimDto) } + }) + @IsObject() + @ValidateNested({ each: true }) + @Transform(({ value }) => + Object.fromEntries(Object.entries(value || {}).map(([k, v]) => [k, plainToInstance(ClaimDto, v)])) + ) + claims: Record; + + @ApiProperty({ type: [String] }) + @IsArray() + credential_signing_alg_values_supported: string[]; + + @ApiProperty({ type: [String] }) + @IsArray() + cryptographic_binding_methods_supported: string[]; + + @ApiProperty({ + description: 'Localized display information for the credential', + type: [DisplayDto] + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DisplayDto) + display: DisplayDto[]; +} + +// export class AuthorizationServerConfigDto { +// @ApiProperty({ +// description: 'Authorization server issuer URL', +// example: 'https://auth.credebl.com', +// }) +// @IsUrl() +// issuer: string + +// @ApiPropertyOptional({ +// description: 'Token endpoint of the authorization server', +// example: 'https://auth.credebl.com/oauth/token', +// }) +// @IsOptional() +// @IsUrl() +// token_endpoint: string + +// @ApiProperty({ +// description: 'Authorization endpoint of the server', +// example: 'https://auth.credebl.com/oauth/authorize', +// }) +// @IsUrl() +// authorization_endpoint: string + +// @ApiProperty({ +// description: 'Supported scopes', +// example: ['openid', 'profile', 'email'], +// }) +// @IsArray() +// @IsString({ each: true }) +// scopes_supported: string[] +// } + +export class ClientAuthenticationDto { + @ApiProperty({ + description: 'OAuth2 client ID for the authorization server', + example: 'issuer-server' + }) + @IsString() + clientId: string; + + @ApiProperty({ + description: 'OAuth2 client secret for the authorization server', + example: '2qKMWulZpMBzXIdfPO5AEs0xaTaKs1uk' + }) + @IsString() + clientSecret: string; +} + +export class AuthorizationServerConfigDto { + @ApiProperty({ + description: 'Authorization server issuer URL', + example: 'https://example-oidc-provider.com' + }) + @IsString() + issuer: string; + + @ApiProperty({ + description: 'Client authentication configuration', + type: () => ClientAuthenticationDto + }) + @ValidateNested() + @Type(() => ClientAuthenticationDto) + clientAuthentication: ClientAuthenticationDto; +} + +export enum AccessTokenSignerKeyType { + ED25519 = 'ed25519' +} + +@ApiExtraModels(CredentialConfigurationDto) +export class IssuerCreationDto { + @ApiProperty({ + description: 'Name of the issuer', + example: 'Credebl University' + }) + @IsString({ message: 'issuerId from IssuerCreationDto -> issuerId, must be a string' }) + issuerId: string; + + @ApiPropertyOptional({ + description: 'Maximum number of credentials that can be issued in a batch', + example: 50, + type: Number + }) + @IsOptional() + @IsInt({ message: 'batchCredentialIssuanceSize must be an integer' }) + batchCredentialIssuanceSize?: number; + + @ApiProperty({ + description: 'Localized display information for the credential', + type: [DisplayDto] + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DisplayDto) + display: DisplayDto[]; + + @ApiProperty({ + description: 'Configuration of the authorization server', + type: AuthorizationServerConfigDto + }) + @ValidateNested() + @Type(() => AuthorizationServerConfigDto) + authorizationServerConfigs: AuthorizationServerConfigDto; +} + +export class IssuerUpdationDto { + issuerId?: string; + + @ApiProperty({ + description: 'accessTokenSignerKeyType', + example: 'ed25519' + }) + @IsString({ + message: 'accessTokenSignerKeyType from IssuerCreationDto -> accessTokenSignerKeyType, must be a string' + }) + accessTokenSignerKeyType: string; + + @ApiProperty({ + description: 'Localized display information for the credential', + type: [DisplayDto] + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DisplayDto) + display: DisplayDto[]; + + @ApiProperty({ + description: 'batchCredentialIssuanceSize', + example: '50' + }) + @ApiPropertyOptional({ + description: 'Maximum number of credentials that can be issued in a batch', + example: 50, + type: Number + }) + @IsOptional() + @IsInt({ message: 'batchCredentialIssuanceSize must be an integer' }) + batchCredentialIssuanceSize?: number; +} diff --git a/apps/api-gateway/src/issuance/issuance.module.ts b/apps/api-gateway/src/issuance/issuance.module.ts index d5f184d16..5a7df978c 100644 --- a/apps/api-gateway/src/issuance/issuance.module.ts +++ b/apps/api-gateway/src/issuance/issuance.module.ts @@ -8,6 +8,7 @@ import { getNatsOptions } from '@credebl/common/nats.config'; import { AwsService } from '@credebl/aws'; import { CommonConstants } from '@credebl/common/common.constant'; import { NATSClient } from '@credebl/common/NATSClient'; +import { OidcController } from './oidc.controller'; @Module({ imports: [ @@ -20,7 +21,7 @@ import { NATSClient } from '@credebl/common/NATSClient'; } ]) ], - controllers: [IssuanceController], + controllers: [IssuanceController, OidcController], providers: [IssuanceService, CommonService, AwsService, NATSClient] }) export class IssuanceModule {} diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index b09b8a4a3..3b0187823 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -1,4 +1,6 @@ /* eslint-disable camelcase */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +// TODO: Remove this import { Injectable, Inject } from '@nestjs/common'; import { BaseService } from 'libs/service/base.service'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; @@ -25,7 +27,7 @@ import { IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; import { IssueCredentialDto } from './dtos/multi-connection.dto'; -import { user } from '@prisma/client'; +import { oidc_issuer, user } from '@prisma/client'; import { NATSClient } from '@credebl/common/NATSClient'; import { ClientProxy } from '@nestjs/microservices'; @Injectable() @@ -270,4 +272,91 @@ export class IssuanceService extends BaseService { }; return this.natsClient.sendNatsMessage(this.issuanceProxy, 'issued-file-data-and-file-details', payload); } + + // OIDC methods + + async oidcIssuerCreate( + issueCredentialDto: IssuerCreationDto, + orgId: string, + userDetails: user + ): Promise { + const payload = { issueCredentialDto, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-issuer-create', payload); + } + + async oidcIssuerUpdate(issueUpdationDto: IssuerUpdationDto, orgId: string, userDetails: user) { + const payload = { issueUpdationDto, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-issuer-update', payload); + } + + async oidcGetIssuerById(issuerId: string, orgId: string) { + const payload = { issuerId, orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-issuer-get-by-id', payload); + } + + async oidcGetIssuers(orgId: string) { + const payload = { orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-get-issuers', payload); + } + + async oidcDeleteIssuer(userDetails: user, orgId: string, issuerId: string) { + const payload = { issuerId, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-delete-issuer', payload); + } + + async deleteTemplate(userDetails: user, orgId: string, templateId: string) { + const payload = { templateId, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-delete', payload); + } + + async updateTemplate( + userDetails: user, + orgId: string, + templateId: string, + dto: UpdateCredentialTemplateDto, + issuerId: string + ) { + const payload = { templateId, orgId, userDetails, dto, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-update', payload); + } + + async findByIdTemplate(userDetails: user, orgId: string, templateId: string) { + const payload = { templateId, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-find-id', payload); + } + + async findAllTemplate(userDetails: user, orgId: string) { + const payload = { orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-find-all', payload); + } + + async createTemplate( + CredentialTemplate: CreateCredentialTemplateDto, + userDetails: user, + orgId: string, + issuerId: string + ) { + const payload = { CredentialTemplate, orgId, userDetails, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-create', payload); + } + + async createOidcCredentialOffer( + oidcCredentialPayload: CreateOidcCredentialOfferDto, + userDetails: user, + orgId: string, + issuerId: string + ) { + const payload = { oidcCredentialPayload, orgId, userDetails, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-create-credential-offer', payload); + } + + oidcIssueCredentialWebhook( + oidcIssueCredentialDto, + id: string + ): Promise<{ + response: object; + }> { + const payload = { oidcIssueCredentialDto, id }; + return this.natsClient.sendNats(this.issuanceProxy, 'webhook-oidc-issue-credential', payload); + } } diff --git a/apps/api-gateway/src/issuance/oidc.controller.ts b/apps/api-gateway/src/issuance/oidc.controller.ts new file mode 100644 index 000000000..b37cc4cb4 --- /dev/null +++ b/apps/api-gateway/src/issuance/oidc.controller.ts @@ -0,0 +1,490 @@ +/* eslint-disable default-param-last */ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-console */ +/* eslint-disable camelcase */ +import { + Controller, + Post, + Body, + UseGuards, + HttpStatus, + Res, + Get, + Param, + UseFilters, + BadRequestException, + ParseUUIDPipe, + Delete, + Patch +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiForbiddenResponse, + ApiUnauthorizedResponse, + ApiExcludeEndpoint +} 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 IResponseType, { IResponse } from '@credebl/common/interfaces/response.interface'; +import { IssuanceService } from './issuance.service'; +import { User } from '../authz/decorators/user.decorator'; +import { ResponseMessages } from '@credebl/common/response-messages'; +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'; +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { user } from '@prisma/client'; +import { IssuerCreationDto, IssuerUpdationDto } from './dtos/oidc-issuer.dto'; +import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oidc-issuer-template.dto'; +import { CreateOidcCredentialOfferDto } from './dtos/issuer-sessions.dto'; +import { IssuanceDto } from './dtos/issuance.dto'; +@Controller() +@UseFilters(CustomExceptionFilter) +@ApiTags('OIDC') +@ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto }) +export class OidcController { + constructor(private readonly issueCredentialService: IssuanceService) {} + /** + * Create issuer against a org(tenant) + * @param orgId The ID of the organization + * @param user The user making the request + * @param res The response object + * @returns The status of the deletion operation + */ + @Post('/orgs/:orgId/oidc/issuers') + @ApiOperation({ summary: 'Create OIDC issuer', description: 'Create OIDC issuer by orgId' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcIssuerCreate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @User() user: user, + @Body() issueCredentialDto: IssuerCreationDto, + @Res() res: Response + ): Promise { + const createIssuer = await this.issueCredentialService.oidcIssuerCreate(issueCredentialDto, orgId, user); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcIssuer.success.issuerConfig, + data: createIssuer + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Post('/orgs/:orgId/oidc/issuers/:issuerId') + @ApiOperation({ summary: 'Update OIDC issuer', description: 'Update OIDC issuer by orgId' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcIssuerUpdate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @User() user: user, + @Param('issuerId') + issuerId: string, + @Body() issueCredentialDto: IssuerUpdationDto, + @Res() res: Response + ): Promise { + issueCredentialDto.issuerId = issuerId; + const createIssuer = await this.issueCredentialService.oidcIssuerUpdate(issueCredentialDto, orgId, user); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcIssuer.success.issuerConfigUpdate, + data: createIssuer + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/orgs/:orgId/oidc/issuers/:issuerId') + @ApiOperation({ summary: 'Get OIDC issuer', description: 'Get OIDC issuer by orgId' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcGetIssuerById( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('issuerId') + issuerId: string, + @Res() res: Response + ): Promise { + const oidcIssuer = await this.issueCredentialService.oidcGetIssuerById(issuerId, orgId); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcIssuer.success.fetch, + data: oidcIssuer + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/orgs/:orgId/oidc/issuers') + @ApiOperation({ summary: 'Get OIDC issuer', description: 'Get OIDC issuer by orgId' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcGetIssuers( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Res() res: Response + ): Promise { + const oidcIssuer = await this.issueCredentialService.oidcGetIssuers(orgId); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcIssuer.success.fetch, + data: oidcIssuer + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Delete('/orgs/:orgId/oidc/:issuerId') + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiOperation({ summary: 'Delete oidc issuer' }) + async deleteOidcIssuer( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param( + 'issuerId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + issuerId: string, + @User() user: user, + @Res() res: Response + ): Promise { + await this.issueCredentialService.oidcDeleteIssuer(user, orgId, issuerId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcIssuer.success.delete + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('/orgs/:orgId/oidc/:issuerId/template') + @ApiOperation({ summary: 'Create credential template' }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Template created successfully' }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async createTemplate( + @Param('orgId') + orgId: string, + @Param('issuerId') + issuerId: string, + @User() user: user, + @Body() CredentialTemplate: CreateCredentialTemplateDto, + @Res() res: Response + ): Promise { + CredentialTemplate.issuerId = issuerId; + console.log('THis is dto', JSON.stringify(CredentialTemplate, null, 2)); + const template = await this.issueCredentialService.createTemplate(CredentialTemplate, user, orgId, issuerId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcTemplate.success.create, + data: template + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/orgs/:orgId/oidc/:issuerId/template') + @ApiOperation({ summary: 'List credential templates' }) + @ApiResponse({ status: HttpStatus.OK, description: 'List of templates' }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async listTemplates( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param( + 'issuerId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + issuerId: string, + @User() user: user, + @Res() res: Response + ): Promise { + const templates = await this.issueCredentialService.findAllTemplate(user, orgId, issuerId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcTemplate.success.fetch, + data: templates + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/orgs/:orgId/oidc/:issuerId/template/:templateId') + @ApiOperation({ summary: 'Get credential template by ID' }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getTemplateById( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param( + 'issuerId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + issuerId: string, + @Param( + 'templateId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + templateId: string, + @User() user: user, + @Res() res: Response + ): Promise { + const template = await this.issueCredentialService.findByIdTemplate(user, orgId, templateId, issuerId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcTemplate.success.fetch, + data: template + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Patch('/orgs/:orgId/oidc/:issuerId/template/:templateId') + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiOperation({ summary: 'Update credential template' }) + async updateTemplate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param( + 'issuerId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + issuerId: string, + @Param( + 'templateId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + templateId: string, + @User() user: user, + @Body() dto: UpdateCredentialTemplateDto, + @Res() res: Response + ): Promise { + const updated = await this.issueCredentialService.updateTemplate(user, orgId, templateId, dto, issuerId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcTemplate.success.update, + data: updated + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Delete('/orgs/:orgId/oidc/:issuerId/template/:templateId') + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiOperation({ summary: 'Delete credential template' }) + async deleteTemplate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param( + 'issuerId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + issuerId: string, + @Param( + 'templateId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + templateId: string, + @User() user: user, + @Res() res: Response + ): Promise { + await this.issueCredentialService.deleteTemplate(user, orgId, templateId, issuerId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcTemplate.success.delete + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('/orgs/:orgId/oidc/:issuerId/create-offer') + @ApiOperation({ summary: 'Create OIDC Credential Offer' }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: `This endpoint creates a new OIDC4VCI credential-offer for a given issuer. It allows clients to request issuance of credentials (e.g., Birth Certificate, Driving License, Student ID) from a registered OIDC issuer using the issuer's ID.` + }) + // @ApiBearerAuth() + // @Roles(OrgRoles.OWNER) + // @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async createOidcCredentialOffer( + @Param('orgId') + orgId: string, + @Param('issuerId') + issuerId: string, + @User() user: user, + @Body() oidcCredentialPayload: CreateOidcCredentialOfferDto, + @Res() res: Response + ): Promise { + oidcCredentialPayload.issuerId = issuerId; + const template = await this.issueCredentialService.createOidcCredentialOffer( + oidcCredentialPayload, + user, + orgId, + issuerId + ); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcTemplate.success.create, + data: template + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Catch issue credential webhook responses + * @param oidcIssueCredentialDto The details of the oidc issued credential + * @param id The ID of the organization + * @param res The response object + * @returns The details of the oidc issued credential + */ + @Post('wh/:id/credentials') + @ApiExcludeEndpoint() + @ApiOperation({ + summary: 'Catch OIDC credential states', + description: 'Catch OIDC credential states' + }) + async getIssueCredentialWebhook( + @Body() oidcIssueCredentialDto, + @Param('id') id: string, + @Res() res: Response + ): Promise { + const getCredentialDetails = await this.issueCredentialService.oidcIssueCredentialWebhook( + oidcIssueCredentialDto, + id + ); + + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.issuance.success.create, + data: getCredentialDetails + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } +} diff --git a/apps/issuance/constant/issuance.ts b/apps/issuance/constant/issuance.ts new file mode 100644 index 000000000..a39a160ca --- /dev/null +++ b/apps/issuance/constant/issuance.ts @@ -0,0 +1,6 @@ +import { AccessTokenSignerKeyType } from '../interfaces/oidc-issuance.interfaces'; + +export const dpopSigningAlgValuesSupported = ['RS256', 'ES256', 'EdDSA']; +export const credentialConfigurationsSupported = {}; +export const accessTokenSignerKeyType = 'ed25519' as AccessTokenSignerKeyType; +export const batchCredentialIssuanceDefault = 0; diff --git a/apps/issuance/interfaces/oidc-issuance.interfaces.ts b/apps/issuance/interfaces/oidc-issuance.interfaces.ts new file mode 100644 index 000000000..854480f70 --- /dev/null +++ b/apps/issuance/interfaces/oidc-issuance.interfaces.ts @@ -0,0 +1,147 @@ +export interface Claim { + key: string; + label: string; + required: boolean; +} + +export interface Logo { + uri: string; + alt_text: string; +} + +export interface Display { + locale: string; + name: string; + description?: string; + logo?: Logo; +} + +export interface CredentialConfiguration { + format: string; + vct?: string; + doctype?: string; + scope: string; + claims: Record; + credential_signing_alg_values_supported: string[]; + cryptographic_binding_methods_supported: string[]; + display: Display[]; +} + +export interface ClientAuthentication { + clientId: string; + clientSecret: string; +} + +export interface AuthorizationServerConfig { + issuer: string; + clientAuthentication: ClientAuthentication; +} + +export interface IssuerCreation { + issuerId: string; + accessTokenSignerKeyType?: AccessTokenSignerKeyType; + display: Display[]; + dpopSigningAlgValuesSupported?: string[]; + credentialConfigurationsSupported?: Record; + authorizationServerConfigs: AuthorizationServerConfig; + batchCredentialIssuanceSize: number; +} + +export interface IssuerInitialConfig { + issuerId: string; + // eslint-disable-next-line @typescript-eslint/ban-types + display: Display[] | {}; + // eslint-disable-next-line @typescript-eslint/ban-types + authorizationServerConfigs: AuthorizationServerConfig | {}; + accessTokenSignerKeyType: AccessTokenSignerKeyType; + dpopSigningAlgValuesSupported: string[]; + batchCredentialIssuance: object; + credentialConfigurationsSupported: object; +} + +export interface IssuerMetadata { + publicIssuerId: string; + createdById: string; + orgAgentId: string; + batchCredentialIssuanceSize?: number; +} + +export interface initialIssuerDetails { + metadata: Display[]; + publicIssuerId: string; +} + +export enum AccessTokenSignerKeyType { + ED25519 = 'ed25519' +} + +export interface IssuerUpdation { + issuerId: string; + accessTokenSignerKeyType: AccessTokenSignerKeyType; + display; + batchCredentialIssuanceSize?: number; +} + +export interface IAgentNatsPayload { + url: string; + apiKey?: string; + orgId?: string; +} + +export type IAgentOIDCIssuerCreate = IAgentNatsPayload & { issuerCreation: IssuerCreation }; + +export interface TagMap { + [key: string]: string; +} + +export interface ClaimDisplay { + name: string; + locale: string; + description?: string; +} + +export interface ClaimDefinition { + value_type: string; + mandatory: boolean; + display: ClaimDisplay[]; +} +export interface Logo { + uri: string; + alt_text: string; +} + +export interface DisplayInfo { + logo?: Logo; + name: string; + locale: string; + description: string; +} + +export interface ClientAuthentication { + clientId: string; + clientSecret: string; +} + +export interface AuthorizationServerConfig { + issuer: string; + clientAuthentication: ClientAuthentication; +} + +export interface BatchCredentialIssuance { + batchSize: number; +} + +export interface IssuerResponse { + _tags: TagMap; + metadata: Record; + id: string; + createdAt: string; + issuerId: string; + accessTokenPublicKeyFingerprint: string; + credentialConfigurationsSupported?: Record; + dpopSigningAlgValuesSupported: string[]; + display?: DisplayInfo[]; + authorizationServerConfigs?: AuthorizationServerConfig[]; + batchCredentialIssuance: BatchCredentialIssuance; + updatedAt: string; +} diff --git a/apps/issuance/interfaces/oidc-issuer-sessions.interfaces.ts b/apps/issuance/interfaces/oidc-issuer-sessions.interfaces.ts new file mode 100644 index 000000000..e6db80dc0 --- /dev/null +++ b/apps/issuance/interfaces/oidc-issuer-sessions.interfaces.ts @@ -0,0 +1,51 @@ +/* --------------------------------------------------------- + * Enums + * --------------------------------------------------------- */ +export enum CredentialFormat { + SdJwtVc = 'vc+sd-jwt', + MsoMdoc = 'mso_mdoc' +} + +export enum SignerMethodOption { + DID = 'did', + X5C = 'x5c' +} + +export interface SignerOption { + method: SignerMethodOption; + did?: string; + x5c?: string[]; +} + +export enum AuthenticationType { + PRE_AUTHORIZED_CODE = 'pre-authorized_code', + AUTHORIZATION_CODE = 'authorization_code' +} + +/* --------------------------------------------------------- + * Interfaces + * --------------------------------------------------------- */ +export type DisclosureFrame = Record>; + +export interface CredentialPayload { + full_name?: string; + birth_date?: string; // YYYY-MM-DD if present + birth_place?: string; + parent_names?: string; + [key: string]: unknown; // extensible for mDoc or other formats +} + +export interface CredentialRequest { + credentialSupportedId: string; + templateId: string; + format: CredentialFormat; // "vc+sd-jwt" | "mso_mdoc" + payload: CredentialPayload; // user-supplied payload (without vct) + disclosureFrame?: DisclosureFrame; // only relevant for vc+sd-jwt +} + +export interface CreateOidcCredentialOffer { + // e.g. "abc-gov" + // signerMethod: SignerMethodOption; // only option selector + authenticationType: AuthenticationType; // only option selector + credentials: CredentialRequest[]; // one or more credentials +} diff --git a/apps/issuance/interfaces/oidc-template.interface.ts b/apps/issuance/interfaces/oidc-template.interface.ts new file mode 100644 index 000000000..2025cfecd --- /dev/null +++ b/apps/issuance/interfaces/oidc-template.interface.ts @@ -0,0 +1,26 @@ +import { Prisma } from '@prisma/client'; +import { Display } from './oidc-issuance.interfaces'; + +export interface CredentialAttribute { + mandatory?: boolean; + value_type: string; + display?: Display[]; +} + +export enum SignerOption { + DID = 'did', + X509 = 'x509' +} +export interface CreateCredentialTemplate { + name: string; + description?: string; + signerOption?: SignerOption; + format: 'sd-jwt-vc' | 'mdoc'; + issuer: string; + canBeRevoked: boolean; + attributes: Prisma.JsonValue; + appearance?: Prisma.JsonValue; + issuerId: string; +} + +export interface UpdateCredentialTemplate extends Partial {} diff --git a/apps/issuance/libs/helpers/credential-sessions.builder.ts b/apps/issuance/libs/helpers/credential-sessions.builder.ts new file mode 100644 index 000000000..6764e0634 --- /dev/null +++ b/apps/issuance/libs/helpers/credential-sessions.builder.ts @@ -0,0 +1,271 @@ +// builder/credential-offer.builder.ts +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ +import { Prisma, credential_templates } from '@prisma/client'; + +/* ============================================================================ + Domain Types +============================================================================ */ + +type ValueType = 'string' | 'date' | 'number' | 'boolean' | string; + +interface TemplateAttribute { + display: { name: string; locale: string }[]; + mandatory: boolean; + value_type: ValueType; +} +type TemplateAttributes = Record; + +export enum CredentialFormat { + SdJwtVc = 'vc+sd-jwt', + Mdoc = 'mdoc' +} + +export enum SignerMethodOption { + DID = 'did', + X5C = 'x5c' +} + +export type DisclosureFrame = Record>; + +export interface CredentialRequestDtoLike { + /** maps to credential_templates.id (the template to use) */ + templateId: string; + /** per-template claims */ + payload: Record; + /** optional selective disclosure map */ + disclosureFrame?: DisclosureFrame; +} + +export interface CreateOidcCredentialOfferDtoLike { + credentials: CredentialRequestDtoLike[]; + + // Exactly one of the two must be provided (XOR) + preAuthorizedCodeFlowConfig?: { + txCode: { description?: string; length: number; input_mode: 'numeric' | 'text' | 'alphanumeric' }; + authorizationServerUrl: string; + }; + authorizationCodeFlowConfig?: { + authorizationServerUrl: string; + }; + + // NOTE: issuerId is intentionally NOT emitted in the final payload + publicIssuerId?: string; +} + +export interface ResolvedSignerOption { + method: 'did' | 'x5c'; + did?: string; + x5c?: string[]; +} + +/* ============================================================================ + Strong return types +============================================================================ */ + +export interface BuiltCredential { + /** e.g., "BirthCertificateCredential-sdjwt" or "DrivingLicenseCredential-mdoc" */ + credentialSupportedId: string; + /** Derived from template.format ("vc+sd-jwt" | "mdoc") */ + format: CredentialFormat; + /** User-provided payload (validated, with vct removed) */ + payload: Record; + /** Optional disclosure frame (usually for SD-JWT) */ + disclosureFrame?: DisclosureFrame; +} + +export interface BuiltCredentialOfferBase { + /** Resolved signer option (DID or x5c) */ + signerOption: ResolvedSignerOption; + /** Normalized credential entries */ + credentials: BuiltCredential[]; + /** Optional public issuer id to include */ + publicIssuerId?: string; +} + +/** Final payload = base + EXACTLY ONE of the two flows */ +export type CredentialOfferPayload = BuiltCredentialOfferBase & + ( + | { + preAuthorizedCodeFlowConfig: { + txCode: { description?: string; length: number; input_mode: 'numeric' | 'text' | 'alphanumeric' }; + authorizationServerUrl: string; + }; + authorizationCodeFlowConfig?: never; + } + | { + authorizationCodeFlowConfig: { + authorizationServerUrl: string; + }; + preAuthorizedCodeFlowConfig?: never; + } + ); + +/* ============================================================================ + Small Utilities +============================================================================ */ + +const isNil = (v: unknown): v is null | undefined => null == v; +const isEmptyString = (v: unknown): boolean => 'string' === typeof v && '' === v.trim(); +const isRecord = (v: unknown): v is Record => Boolean(v) && 'object' === typeof v && !Array.isArray(v); + +/** Map DB format string -> API enum */ +function mapDbFormatToApiFormat(db: string): CredentialFormat { + const norm = db.toLowerCase().replace(/_/g, '-'); + if ('sd-jwt' === norm || 'vc+sd-jwt' === norm || 'sdjwt' === norm) { + return CredentialFormat.SdJwtVc; + } + if ('mdoc' === norm || 'mso-mdoc' === norm || 'mso-mdoc' === norm) { + return CredentialFormat.Mdoc; + } + throw new Error(`Unsupported template format: ${db}`); +} + +/** Map API enum -> id suffix required for credentialSupportedId */ +function formatSuffix(api: CredentialFormat): 'sdjwt' | 'mdoc' { + return api === CredentialFormat.SdJwtVc ? 'sdjwt' : 'mdoc'; +} + +/* ============================================================================ + Validation of Payload vs Template Attributes +============================================================================ */ + +/** Throw if any template-mandatory claim is missing/empty in payload. */ +function assertMandatoryClaims( + payload: Record, + attributes: TemplateAttributes, + ctx: { templateId: string } +): void { + const missing: string[] = []; + for (const [claim, def] of Object.entries(attributes)) { + if (!def?.mandatory) { + continue; + } + const val = payload[claim]; + if (isNil(val) || isEmptyString(val)) { + missing.push(claim); + } + } + if (missing.length) { + throw new Error(`Missing mandatory claims for template "${ctx.templateId}": ${missing.join(', ')}`); + } +} + +/* ============================================================================ + JsonValue → TemplateAttributes Narrowing (Type Guards) +============================================================================ */ + +function isDisplayArray(v: unknown): v is { name: string; locale: string }[] { + return Array.isArray(v) && v.every((d) => isRecord(d) && 'string' === typeof d.name && 'string' === typeof d.locale); +} + +function isTemplateAttribute(v: unknown): v is TemplateAttribute { + return ( + isRecord(v) && isDisplayArray(v.display) && 'boolean' === typeof v.mandatory && 'string' === typeof v.value_type + ); +} + +/** Accept `unknown` so predicate type (TemplateAttributes) is assignable to parameter type. */ +function isTemplateAttributes(v: unknown): v is TemplateAttributes { + if (!isRecord(v)) { + return false; + } + return Object.values(v).every(isTemplateAttribute); +} + +/** Runtime assert + narrow Prisma.JsonValue → TemplateAttributes */ +function ensureTemplateAttributes(v: Prisma.JsonValue): TemplateAttributes { + if (!isTemplateAttributes(v)) { + throw new Error('Invalid template.attributes shape. Expecting TemplateAttributes map.'); + } + return v; +} + +/* ============================================================================ + Builders +============================================================================ */ + +/** Build one credential block normalized to API format (using the template's format). */ +function buildOneCredential( + cred: CredentialRequestDtoLike, + template: credential_templates, + attrs: TemplateAttributes +): BuiltCredential { + // 1) Validate payload against template attributes + assertMandatoryClaims(cred.payload, attrs, { templateId: cred.templateId }); + + // 2) Decide API format from DB format + const apiFormat = mapDbFormatToApiFormat(template.format); + + // 3) Build supportedId from template.name + suffix ("-sdjwt" | "-mdoc") + const suffix = formatSuffix(apiFormat); + const credentialSupportedId = `${template.name}-${suffix}`; + + // 4) Strip vct ALWAYS (per requirement) + const payload = { ...(cred.payload as Record) }; + delete (payload as Record).vct; + + return { + credentialSupportedId, // e.g., "BirthCertificateCredential-sdjwt" + format: apiFormat, // 'vc+sd-jwt' | 'mdoc' + payload, // without vct + ...(cred.disclosureFrame ? { disclosureFrame: cred.disclosureFrame } : {}) + }; +} + +/** + * Build the full OIDC credential offer payload. + * - Verifies template IDs + * - Validates mandatory claims per template + * - Normalizes formats & IDs + * - Enforces XOR of flow configs + * - Removes issuerId from the final envelope + * - Removes vct from all payloads + * - Sets credentialSupportedId = "-sdjwt|mdoc" + */ +export function buildCredentialOfferPayload( + dto: CreateOidcCredentialOfferDtoLike, + templates: credential_templates[], + signerOption: ResolvedSignerOption +): CredentialOfferPayload { + // Index templates + const byId = new Map(templates.map((t) => [t.id, t])); + + // Verify all requested templateIds exist + const unknown = dto.credentials.map((c) => c.templateId).filter((id) => !byId.has(id)); + if (unknown.length) { + throw new Error(`Unknown template ids: ${unknown.join(', ')}`); + } + + // Build credentials + const credentials: BuiltCredential[] = dto.credentials.map((cred) => { + const template = byId.get(cred.templateId)!; + const attrs = ensureTemplateAttributes(template.attributes); // narrow JsonValue safely + return buildOneCredential(cred, template, attrs); + }); + + // --- Base envelope (issuerId deliberately NOT included) --- + const base: BuiltCredentialOfferBase = { + signerOption, // resolved keys (did/x5c) from DB + credentials, + ...(dto.publicIssuerId ? { publicIssuerId: dto.publicIssuerId } : {}) + }; + + // XOR flow selection (defensive) + const hasPre = Boolean(dto.preAuthorizedCodeFlowConfig); + const hasAuth = Boolean(dto.authorizationCodeFlowConfig); + if (hasPre === hasAuth) { + throw new Error('Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'); + } + + if (hasPre) { + return { + ...base, + preAuthorizedCodeFlowConfig: dto.preAuthorizedCodeFlowConfig! // definite since hasPre + }; + } + + return { + ...base, + authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! // definite since !hasPre + }; +} diff --git a/apps/issuance/libs/helpers/issuer.metadata.ts b/apps/issuance/libs/helpers/issuer.metadata.ts new file mode 100644 index 000000000..f9950f807 --- /dev/null +++ b/apps/issuance/libs/helpers/issuer.metadata.ts @@ -0,0 +1,244 @@ +/* eslint-disable camelcase */ +import { oidc_issuer, Prisma } from '@prisma/client'; +import { batchCredentialIssuanceDefault } from '../../constant/issuance'; +import { CreateOidcCredentialOffer } from 'apps/issuance/interfaces/oidc-issuer-sessions.interfaces'; + +type AttributeDisplay = { name: string; locale: string }; +type AttributeDef = { + display?: AttributeDisplay[]; + mandatory?: boolean; + value_type: 'string' | 'date' | 'number' | 'boolean' | string; +}; +type AttributesMap = Record; + +type CredentialDisplayItem = { + logo?: { uri: string; alt_text?: string }; + name: string; + locale?: string; + description?: string; +}; +type Appearance = { + display: CredentialDisplayItem[]; +}; + +type CredentialConfig = { + format: string; + vct?: string; + scope: string; + doctype?: string; + claims: Record< + string, + { + mandatory?: boolean; + value_type: string; + display?: AttributeDisplay[]; + } + >; + credential_signing_alg_values_supported: string[]; + cryptographic_binding_methods_supported: string[]; + display: { name: string; description?: string; locale?: string }[]; +}; + +type CredentialConfigurationsSupported = { + credentialConfigurationsSupported: Record; +}; + +// ---- Static Lists (as requested) ---- +const STATIC_CREDENTIAL_ALGS = ['ES256', 'EdDSA'] as const; +const STATIC_BINDING_METHODS = ['did:key'] as const; +const DOCTYPE = 'org.iso.18013.5.1'; // for mso_mdoc format +const MSO_MDOC = 'mso_mdoc'; // alternative format value + +// 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; +}; + +/** + * Build agent payload from Prisma rows (attributes/appearance are Prisma.JsonValue). + * Safely coerces JSON and then builds the same structure as Builder #2. + */ +export function buildCredentialConfigurationsSupported( + templates: TemplateRowPrisma[], + opts?: { + vct?: string; + scopeVct?: string; + keyResolver?: (t: TemplateRowPrisma) => string; + format?: string; + } +): CredentialConfigurationsSupported { + const format = opts?.format ?? 'vc+sd-jwt'; + const credentialConfigurationsSupported: Record = {}; + for (const t of templates) { + // Coerce JSON fields + const attrs = coerceJsonObject(t.attributes); + const app = coerceJsonObject(t.appearance); + + if (!isAttributesMap(attrs)) { + throw new Error(`Template ${t.id}: invalid attributes JSON`); + } + if (!isAppearance(app)) { + throw new Error(`Template ${t.id}: invalid appearance JSON (missing display)`); + } + + // ---- dynamic format per row ---- + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rowFormat = (t as any).format ?? format; + const suffix = rowFormat === `${MSO_MDOC}` ? 'mdoc' : 'sdjwt'; + + // key: keep your keyResolver override; otherwise include suffix + const key = 'function' === typeof opts?.keyResolver ? opts.keyResolver(t) : `${t.name}-${suffix}`; + + // vct/scope: vct only for non-mdoc; scope always uses suffix + const vct = opts?.vct ?? t.name; + const scopeBase = opts?.scopeVct ?? vct; + const scope = `openid4vc:credential:${scopeBase}-${suffix}`; + const claims = Object.fromEntries( + Object.entries(attrs).map(([claimName, def]) => { + const d = def as AttributeDef; + return [ + claimName, + { + value_type: d.value_type, + mandatory: d.mandatory ?? false, // always include, default to false + display: Array.isArray(d.display) ? d.display.map((x) => ({ name: x.name, locale: x.locale })) : undefined + } + ]; + }) + ); + + const display = + app.display.map((d) => ({ + name: d.name, + description: d.description, + locale: d.locale + })) ?? []; + + // assemble per-template config + credentialConfigurationsSupported[key] = { + format: rowFormat, + scope, + claims, + credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], + cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], + display, + ...(rowFormat === `${MSO_MDOC}` + ? { doctype: `${DOCTYPE}` } // static for mdoc + : { vct }) // keep vct only for non-mdoc + }; + } + + return { + credentialConfigurationsSupported + }; +} + +// Default DPoP list for issuer-level metadata (match your example) +const ISSUER_DPOP_ALGS_DEFAULT = ['RS256', 'ES256'] as const; + +// ---------- 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; +} + +type DisplayItem = { + name: string; + locale?: string; + description?: 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 + ) + ); +} + +// ---------- Builder you asked for ---------- +/** + * Build issuer metadata payload from issuer row + credential configurations. + * + * @param credentialConfigurations Object with credentialConfigurationsSupported (from your template builder) + * @param oidcIssuer OIDC issuer row (uses publicIssuerId and metadata -> display) + * @param opts Optional overrides: dpopAlgs[], accessTokenSignerKeyType + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildIssuerPayload( + credentialConfigurations: CredentialConfigurationsSupported, + oidcIssuer: oidc_issuer, + opts?: { + dpopAlgs?: string[]; + accessTokenSignerKeyType?: string; + } +) { + if (!oidcIssuer?.publicIssuerId || 'string' !== typeof oidcIssuer.publicIssuerId) { + throw new Error('Invalid issuer: missing publicIssuerId'); + } + + const rawDisplay = coerceJson(oidcIssuer.metadata); + const display: DisplayItem[] = isDisplayArray(rawDisplay) ? rawDisplay : []; + + return { + display, + dpopSigningAlgValuesSupported: opts?.dpopAlgs ?? [...ISSUER_DPOP_ALGS_DEFAULT], + credentialConfigurationsSupported: credentialConfigurations.credentialConfigurationsSupported ?? {}, + batchCredentialIssuance: { + batchSize: oidcIssuer?.batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault + } + }; +} + +export function extractTemplateIds(offer: CreateOidcCredentialOffer): string[] { + if (!offer?.credentials || !Array.isArray(offer.credentials)) { + return []; + } + + return offer.credentials.map((c) => c.templateId).filter((id): id is string => Boolean(id)); +} diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index b1bbf6bcc..520637516 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ +// TODO: Remove this import { IClientDetails, IIssuance, @@ -19,12 +21,19 @@ import { Controller, Logger } from '@nestjs/common'; import { IssuanceService } from './issuance.service'; import { MessagePattern } from '@nestjs/microservices'; import { OOBIssueCredentialDto } from 'apps/api-gateway/src/issuance/dtos/issuance.dto'; -import { user } from '@prisma/client'; +import { credential_templates, oidc_issuer, user } from '@prisma/client'; +import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oidc-template.interface'; +import { IssuerCreation, IssuerUpdation } from '../interfaces/oidc-issuance.interfaces'; +import { CreateOidcCredentialOffer } from '../interfaces/oidc-issuer-sessions.interfaces'; +import { OIDCIssuanceService } from './oidc-issuance.service'; @Controller() export class IssuanceController { private readonly logger = new Logger('IssueCredentialController'); - constructor(private readonly issuanceService: IssuanceService) {} + constructor( + private readonly issuanceService: IssuanceService, + private readonly oidcIssuanceService: OIDCIssuanceService + ) {} @MessagePattern({ cmd: 'get-issuance-records' }) async getIssuanceRecordsByOrgId(payload: { orgId: string; userId: string }): Promise { @@ -135,4 +144,121 @@ export class IssuanceController { async getFileDetailsAndFileDataByFileId(payload: { fileId: string; orgId: string }): Promise { return this.issuanceService.getFileDetailsAndFileDataByFileId(payload.fileId, payload.orgId); } + + @MessagePattern({ cmd: 'oidc-issuer-create' }) + async oidcIssuerCreate(payload: { + issueCredentialDto: IssuerCreation; + orgId: string; + userDetails: user; + }): Promise { + const { issueCredentialDto, orgId, userDetails } = payload; + return this.oidcIssuanceService.oidcIssuerCreate(issueCredentialDto, orgId, userDetails); + } + + @MessagePattern({ cmd: 'oidc-issuer-update' }) + async oidcIssuerUpdate(payload: { + issueUpdationDto: IssuerUpdation; + orgId: string; + userDetails: user; + }): Promise { + const { issueUpdationDto, orgId, userDetails } = payload; + return this.oidcIssuanceService.oidcIssuerUpdate(issueUpdationDto, orgId, userDetails); + } + + @MessagePattern({ cmd: 'oidc-issuer-get-by-id' }) + async oidcGetIssuerById(payload: { + issuerId: string; + orgId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { issuerId, orgId } = payload; + return this.oidcIssuanceService.oidcIssuerGetById(issuerId, orgId); + } + + @MessagePattern({ cmd: 'oidc-get-issuers' }) + async oidcGetIssuers(payload: { orgId: string }): Promise { + const { orgId } = payload; + return this.oidcIssuanceService.oidcIssuers(orgId); + } + + @MessagePattern({ cmd: 'oidc-delete-issuer' }) + async deleteOidcIssuer(payload: { + orgId: string; + userDetails: user; + issuerId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { orgId, userDetails, issuerId } = payload; + return this.oidcIssuanceService.deleteOidcIssuer(orgId, userDetails, issuerId); + } + + @MessagePattern({ cmd: 'oidc-template-create' }) + async oidcTemplateCreate(payload: { + CredentialTemplate: CreateCredentialTemplate; + orgId: string; + issuerId: string; + }): Promise { + const { CredentialTemplate, orgId, issuerId } = payload; + return this.oidcIssuanceService.createTemplate(CredentialTemplate, orgId, issuerId); + } + + @MessagePattern({ cmd: 'oidc-template-update' }) + async oidcTemplateUpdate(payload: { + templateId: string; + dto: UpdateCredentialTemplate; + orgId: string; + issuerId: string; + }): Promise { + const { templateId, dto, orgId, issuerId } = payload; + return this.oidcIssuanceService.updateTemplate(templateId, dto, orgId, issuerId); + } + + @MessagePattern({ cmd: 'oidc-template-delete' }) + async oidcTemplateDelete(payload: { + templateId: string; + orgId: string; + userDetails: user; + issuerId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { templateId, orgId, userDetails, issuerId } = payload; + return this.oidcIssuanceService.deleteTemplate(templateId, orgId, userDetails, issuerId); + } + + @MessagePattern({ cmd: 'oidc-template-find-id' }) + async oidcTemplateFindById(payload: { + templateId: string; + orgId: string; + userDetails: user; + issuerId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { templateId, orgId, userDetails, issuerId } = payload; + return this.oidcIssuanceService.findByIdTemplate(templateId, orgId, userDetails, issuerId); + } + + @MessagePattern({ cmd: 'oidc-template-find-all' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcTemplateFindAll(payload: { orgId: string; userDetails: user; issuerId: string }): Promise { + const { orgId, userDetails, issuerId } = payload; + return this.oidcIssuanceService.findAllTemplate(orgId, userDetails, issuerId); + } + + @MessagePattern({ cmd: 'oidc-create-credential-offer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async createOidcCredentialOffer(payload: { + oidcCredentialPayload: CreateOidcCredentialOffer; + orgId: string; + userDetails: user; + issuerId: string; + }): Promise { + const { oidcCredentialPayload, orgId, userDetails, issuerId } = payload; + return this.oidcIssuanceService.createOidcCredentialOffer(oidcCredentialPayload, orgId, userDetails, issuerId); + } + + //TODO: complete the logic + @MessagePattern({ cmd: 'webhook-oidc-issue-credential' }) + async oidcIssueCredentialWebhook(payload: IssueCredentialWebhookPayload): Promise { + return this.issuanceService.getIssueCredentialWebhook(payload); + } } diff --git a/apps/issuance/src/issuance.module.ts b/apps/issuance/src/issuance.module.ts index 76e5cf912..b72a0d6a6 100644 --- a/apps/issuance/src/issuance.module.ts +++ b/apps/issuance/src/issuance.module.ts @@ -20,6 +20,7 @@ import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { ContextInterceptorModule } from '@credebl/context/contextInterceptorModule'; import { GlobalConfigModule } from '@credebl/config/global-config.module'; import { NATSClient } from '@credebl/common/NATSClient'; +import { OIDCIssuanceService } from './oidc-issuance.service'; @Module({ imports: [ @@ -50,6 +51,7 @@ import { NATSClient } from '@credebl/common/NATSClient'; controllers: [IssuanceController], providers: [ IssuanceService, + OIDCIssuanceService, IssuanceRepository, UserActivityRepository, PrismaService, diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index 8fa95635a..6258a47af 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -14,9 +14,11 @@ import { IssueCredentials, IssuedCredentialStatus } from '../enum/issuance.enum' import { Prisma, agent_invitations, + credential_templates, credentials, file_data, file_upload, + oidc_issuer, org_agents, organisation, platform_config, @@ -30,6 +32,7 @@ import { IIssuedCredentialSearchParams } from 'apps/api-gateway/src/issuance/int import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { PrismaService } from '@credebl/prisma-service'; import { ResponseMessages } from '@credebl/common/response-messages'; +import { IssuerMetadata, IssuerUpdation } from '../interfaces/oidc-issuance.interfaces'; @Injectable() export class IssuanceRepository { @@ -793,4 +796,186 @@ export class IssuanceRepository { throw error; } } + + async getOidcIssuerByOrg(orgId: string): Promise { + try { + return await this.prisma.oidc_issuer.findMany({ + where: { createdBy: orgId }, + include: { + templates: true + }, + orderBy: { + createDateTime: 'desc' + } + }); + } catch (error) { + this.logger.error(`Error in getOidcIssuerByOrg: ${error.message}`); + throw error; + } + } + + async getOidcIssuerDetailsById(issuerId: string): Promise { + try { + return await this.prisma.oidc_issuer.findFirstOrThrow({ + where: { id: issuerId } + }); + } catch (error) { + this.logger.error(`Error in getOidcIssuerDetailsById: ${error.message}`); + throw error; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async addOidcIssuerDetails(issuerMetadata: IssuerMetadata, issuerProfileJson): Promise { + try { + const { publicIssuerId, createdById, orgAgentId, batchCredentialIssuanceSize } = issuerMetadata; + const oidcIssuerDetails = await this.prisma.oidc_issuer.create({ + data: { + metadata: issuerProfileJson, + publicIssuerId, + createdBy: createdById, + orgAgentId, + batchCredentialIssuanceSize + } + }); + + return oidcIssuerDetails; + } catch (error) { + this.logger.error(`[addOidcIssuerDetails] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async updateOidcIssuerDetails(createdById: string, issuerConfig: IssuerUpdation): Promise { + try { + const { issuerId, display, batchCredentialIssuanceSize } = issuerConfig; + const oidcIssuerDetails = await this.prisma.oidc_issuer.update({ + where: { id: issuerId }, + data: { + metadata: display as unknown as Prisma.InputJsonValue, + createdBy: createdById, + ...(batchCredentialIssuanceSize !== undefined ? { batchCredentialIssuanceSize } : {}) + } + }); + + return oidcIssuerDetails; + } catch (error) { + this.logger.error(`[addOidcIssuerDetails] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async deleteOidcIssuer(issuerId: string): Promise { + try { + return await this.prisma.oidc_issuer.delete({ + where: { id: issuerId } + }); + } catch (error) { + this.logger.error(`[deleteOidcIssuer] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async createTemplate( + issuerId: string, + data: Omit + ): Promise { + try { + return await this.prisma.credential_templates.create({ + data: { + ...data, + issuerId + } + }); + } catch (error) { + this.logger.error(`Error in createTemplate: ${error.message}`); + throw error; + } + } + + async getTemplateById(templateId: string): Promise { + try { + return await this.prisma.credential_templates.findUnique({ + where: { id: templateId } + }); + } catch (error) { + this.logger.error(`Error in getTemplateById: ${error.message}`); + throw error; + } + } + + async getTemplateByIds(templateIds: string[], issuerId: string): Promise { + try { + // Early return if empty input (avoids full table scan if someone passes []) + if (!Array.isArray(templateIds) || 0 === templateIds.length) { + return []; + } + + this.logger.debug(`getTemplateByIds templateIds=${JSON.stringify(templateIds)} issuerId=${issuerId}`); + + return await this.prisma.credential_templates.findMany({ + where: { + id: { in: templateIds }, + issuerId + } + }); + } catch (error) { + this.logger.error(`Error in getTemplateByIds: ${error?.message}`, error?.stack); + throw error; + } + } + + async getTemplateByNameForIssuer(name: string, issuerId: string): Promise { + try { + return await this.prisma.credential_templates.findMany({ + where: { + issuerId, + name: { + equals: name, + mode: 'insensitive' + } + } + }); + } catch (error) { + this.logger.error(`Error in getTemplateByNameForIssuer: ${error.message}`); + throw error; + } + } + + async getTemplatesByIssuerId(issuerId: string): Promise { + try { + return await this.prisma.credential_templates.findMany({ + where: { issuerId }, + orderBy: { + createdAt: 'desc' + } + }); + } catch (error) { + this.logger.error(`Error in getTemplatesByIssuer: ${error.message}`); + throw error; + } + } + + async updateTemplate(templateId: string, data: Partial): Promise { + try { + return await this.prisma.credential_templates.update({ + where: { id: templateId }, + data + }); + } catch (error) { + this.logger.error(`Error in updateTemplate: ${error.message}`); + throw error; + } + } + + async deleteTemplate(templateId: string): Promise { + try { + return await this.prisma.credential_templates.delete({ + where: { id: templateId } + }); + } catch (error) { + this.logger.error(`Error in deleteTemplate: ${error.message}`); + throw error; + } + } } diff --git a/apps/issuance/src/oidc-issuance.service.ts b/apps/issuance/src/oidc-issuance.service.ts new file mode 100644 index 000000000..da743deb6 --- /dev/null +++ b/apps/issuance/src/oidc-issuance.service.ts @@ -0,0 +1,622 @@ +/* eslint-disable quotes */ +/* eslint-disable no-useless-catch */ +/* eslint-disable camelcase */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ + +import { + BadRequestException, + ConflictException, + HttpException, + Inject, + Injectable, + InternalServerErrorException, + Logger, + NotFoundException, + Scope +} from '@nestjs/common'; +import { IssuanceRepository } from './issuance.repository'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { map } from 'rxjs'; +import { getAgentUrl } from '@credebl/common/common.utils'; +import { credential_templates, oidc_issuer, user } from '@prisma/client'; +import { + IAgentOIDCIssuerCreate, + IssuerCreation, + IssuerInitialConfig, + IssuerMetadata, + IssuerResponse, + IssuerUpdation +} from '../interfaces/oidc-issuance.interfaces'; +import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oidc-template.interface'; +import { + accessTokenSignerKeyType, + batchCredentialIssuanceDefault, + credentialConfigurationsSupported, + dpopSigningAlgValuesSupported +} from '../constant/issuance'; +import { + buildCredentialConfigurationsSupported, + buildIssuerPayload, + extractTemplateIds +} from '../libs/helpers/issuer.metadata'; +import { + CreateOidcCredentialOffer, + SignerMethodOption, + SignerOption +} from '../interfaces/oidc-issuer-sessions.interfaces'; +import { BadRequestErrorDto } from 'apps/api-gateway/src/dtos/bad-request-error.dto'; +import { buildCredentialOfferPayload, CredentialOfferPayload } from '../libs/helpers/credential-sessions.builder'; + +type CredentialDisplayItem = { + logo?: { uri: string; alt_text?: string }; + name: string; + locale?: string; + description?: string; +}; + +type Appearance = { + display: CredentialDisplayItem[]; +}; +@Injectable() +export class OIDCIssuanceService { + private readonly logger = new Logger('IssueCredentialService'); + constructor( + @Inject('NATS_CLIENT') private readonly issuanceServiceProxy: ClientProxy, + private readonly issuanceRepository: IssuanceRepository + ) {} + + async oidcIssuerCreate(issuerCreation: IssuerCreation, orgId: string, userDetails: user): Promise { + try { + const { issuerId, batchCredentialIssuanceSize } = issuerCreation; + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id: orgAgentId, orgAgentTypeId } = agentDetails; + const orgAgentType = await this.issuanceRepository.getOrgAgentType(orgAgentTypeId); + if (!orgAgentType) { + throw new NotFoundException(ResponseMessages.issuance.error.orgAgentTypeNotFound); + } + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_CREATE); + const issuerInitialConfig: IssuerInitialConfig = { + issuerId, + display: issuerCreation?.display || {}, + authorizationServerConfigs: issuerCreation?.authorizationServerConfigs || {}, + accessTokenSignerKeyType, + dpopSigningAlgValuesSupported, + batchCredentialIssuance: { + batchSize: batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault + }, + credentialConfigurationsSupported + }; + let createdIssuer; + try { + createdIssuer = await this._createOIDCIssuer(issuerInitialConfig, url, orgId); + } catch (error) { + this.logger.error(`[oidcIssuerCreate] - error in oidcIssuerCreate issuance records: ${JSON.stringify(error)}`); + const status409 = + 409 === error?.status?.message?.statusCode || 409 === error?.response?.status || 409 === error?.statusCode; + + if (status409) { + throw new ConflictException(`Issuer with id '${issuerCreation.issuerId}' already exists`); + } + throw error; + } + const issuerConfigJson = createdIssuer?.response ?? createdIssuer ?? {}; + const issuerIdFromAgent = issuerConfigJson?.issuerId; + + if (!issuerIdFromAgent) { + throw new InternalServerErrorException('Issuer ID missing from agent response'); + } + const issuerMetadata: IssuerMetadata = { + publicIssuerId: issuerIdFromAgent, + createdById: userDetails.id, + orgAgentId, + batchCredentialIssuanceSize: issuerCreation?.batchCredentialIssuanceSize + }; + const addOidcIssuerDetails = await this.issuanceRepository.addOidcIssuerDetails( + issuerMetadata, + issuerCreation?.display + ); + + if (!addOidcIssuerDetails) { + throw new InternalServerErrorException('Error in adding OIDC Issuer details in DB'); + } + return addOidcIssuerDetails; + } catch (error) { + this.logger.error(`[oidcIssuerCreate] - error in oidcIssuerCreate issuance records: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async oidcIssuerUpdate(issuerUpdationConfig: IssuerUpdation, orgId: string, userDetails: user): Promise { + try { + const getIssuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerUpdationConfig.issuerId); + if (!getIssuerDetails) { + throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); + } + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id: orgAgentId, orgAgentTypeId } = agentDetails; + const orgAgentType = await this.issuanceRepository.getOrgAgentType(orgAgentTypeId); + if (!orgAgentType) { + throw new NotFoundException(ResponseMessages.issuance.error.orgAgentTypeNotFound); + } + + const addOidcIssuerDetails = await this.issuanceRepository.updateOidcIssuerDetails( + userDetails.id, + issuerUpdationConfig + ); + + if (!addOidcIssuerDetails) { + throw new InternalServerErrorException('Error in updating OIDC Issuer details in DB'); + } + + const url = await getAgentUrl( + agentEndPoint, + CommonConstants.OIDC_ISSUER_TEMPLATE, + getIssuerDetails.publicIssuerId + ); + const issuerConfig = await this.buildOidcIssuerConfig(issuerUpdationConfig.issuerId); + const updatedIssuer = await this._createOIDCTemplate(issuerConfig, url, orgId); + if (updatedIssuer?.response?.statusCode && 200 !== updatedIssuer?.response?.statusCode) { + throw new InternalServerErrorException( + `Error from agent while updating issuer: ${updatedIssuer?.response?.message ?? 'Unknown error'}` + ); + } + + return addOidcIssuerDetails; + } catch (error) { + this.logger.error(`[oidcIssuerUpdate] - error in oidcIssuerUpdate issuance records: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async oidcIssuerGetById(issuerId: string, orgId: string): Promise { + try { + const getIssuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!getIssuerDetails && getIssuerDetails.publicIssuerId) { + throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); + } + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const url = await getAgentUrl( + agentDetails?.agentEndPoint, + CommonConstants.OIDC_ISSUER_BY_ID, + getIssuerDetails?.publicIssuerId + ); + const issuerDetailsRaw = await this._oidcGetIssuerById(url, orgId); + + if (!issuerDetailsRaw) { + throw new InternalServerErrorException(`Error from agent while getting issuer`); + } + + // response is a string → parse it into IssuerResponse + const issuerDetails = { + response: JSON.parse(issuerDetailsRaw.response) as IssuerResponse + }; + + return issuerDetails.response; + } catch (error) { + this.logger.error(`[oidcIssuerGetById] - error in oidcIssuerGetById issuance records: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async oidcIssuers(orgId: string): Promise { + try { + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const url = await getAgentUrl(agentDetails?.agentEndPoint, CommonConstants.OIDC_GET_ALL_ISSUERS); + const issuersDetails = await this._oidcGetIssuers(url, orgId); + if (!issuersDetails) { + throw new InternalServerErrorException(`Error from agent while oidcIssuers`); + } + + return issuersDetails; + } catch (error) { + this.logger.error(`[oidcIssuers] - error in oidcIssuers issuance records: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async deleteOidcIssuer(orgId: string, userDetails: user, issuerId: string) { + try { + const deleteOidcIssuer = await this.issuanceRepository.deleteOidcIssuer(issuerId); + if (!deleteOidcIssuer) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.deleteTemplate); + } + + const issuerRecordId = await this.oidcIssuerGetById(issuerId, orgId); + if (!issuerRecordId.id) { + throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); + } + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint } = agentDetails; + const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!issuerDetails) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + } + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_DELETE, issuerRecordId.id); + + const createTemplateOnAgent = await this._deleteOidcIssuer(url, orgId); + if (!createTemplateOnAgent) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + return deleteOidcIssuer; + } catch (error) { + this.logger.error(`[deleteTemplate] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async createTemplate( + CredentialTemplate: CreateCredentialTemplate, + orgId: string, + issuerId: string + ): Promise { + try { + const { name, description, format, canBeRevoked, attributes, appearance, signerOption } = CredentialTemplate; + const checkNameExist = await this.issuanceRepository.getTemplateByNameForIssuer(name, issuerId); + if (0 < checkNameExist.length) { + throw new ConflictException(ResponseMessages.oidcTemplate.error.templateNameAlreadyExist); + } + const metadata = { + name, + description, + format, + canBeRevoked, + attributes, + appearance: appearance ?? {}, + issuerId, + signerOption + }; + // Persist in DB + const createdTemplate = await this.issuanceRepository.createTemplate(issuerId, metadata); + if (!createdTemplate) { + throw new InternalServerErrorException(ResponseMessages.oidcTemplate.error.createFailed); + } + const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint } = agentDetails; + const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!issuerDetails) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + } + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_TEMPLATE, issuerDetails.publicIssuerId); + const createTemplateOnAgent = await this._createOIDCTemplate(issuerTemplateConfig, url, orgId); + if (!createTemplateOnAgent) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + return createdTemplate; + } catch (error) { + this.logger.error(`[createTemplate] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async updateTemplate( + templateId: string, + updateCredentialTemplate: UpdateCredentialTemplate, + orgId: string, + issuerId: string + ): Promise { + try { + const template = await this.issuanceRepository.getTemplateById(templateId); + if (!template) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.notFound); + } + if (updateCredentialTemplate.name) { + const checkNameExist = await this.issuanceRepository.getTemplateByNameForIssuer( + updateCredentialTemplate.name, + issuerId + ); + if (0 < checkNameExist.length) { + throw new ConflictException(ResponseMessages.oidcTemplate.error.templateNameAlreadyExist); + } + } + const normalized = { + ...updateCredentialTemplate, + ...(issuerId ? { issuerId } : {}) + }; + const { name, description, format, canBeRevoked, attributes, appearance } = normalized; + + const payload = { + ...(name !== undefined ? { name } : {}), + ...(description !== undefined ? { description } : {}), + ...(format !== undefined ? { format } : {}), + ...(canBeRevoked !== undefined ? { canBeRevoked } : {}), + ...(attributes !== undefined ? { attributes } : {}), + ...(appearance !== undefined ? { appearance } : {}), + ...(issuerId ? { issuerId } : {}) + }; + + const updatedTemplate = await this.issuanceRepository.updateTemplate(templateId, payload); + + const templates = await this.issuanceRepository.getTemplatesByIssuerId(issuerId); + if (!templates || 0 === templates.length) { + throw new NotFoundException(ResponseMessages.issuance.error.notFound); + } + const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint } = agentDetails; + const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!issuerDetails) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + } + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_TEMPLATE, issuerDetails.publicIssuerId); + + const createTemplateOnAgent = await this._createOIDCTemplate(issuerTemplateConfig, url, orgId); + if (!createTemplateOnAgent) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + return updatedTemplate; + } catch (error) { + this.logger.error(`[updateTemplate] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async deleteTemplate(templateId: string, orgId: string, userDetails: user, issuerId: string): Promise { + try { + const template = await this.issuanceRepository.getTemplateById(templateId); + if (!template) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.notFound); + } + const deleteTemplate = await this.issuanceRepository.deleteTemplate(templateId); + if (!deleteTemplate) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.deleteTemplate); + } + + const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint } = agentDetails; + const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!issuerDetails) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + } + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_TEMPLATE, issuerDetails.publicIssuerId); + + const createTemplateOnAgent = await this._createOIDCTemplate(issuerTemplateConfig, url, orgId); + if (!createTemplateOnAgent) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + return deleteTemplate; + } catch (error) { + this.logger.error(`[deleteTemplate] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async findByIdTemplate(templateId: string, orgId: string, userDetails: user, issuerId: string): Promise { + try { + const template = await this.issuanceRepository.getTemplateById(templateId); + if (!template) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.notFound); + } + + return { message: ResponseMessages.oidcTemplate.success.fetch, data: template }; + } catch (error) { + this.logger.error(`[findByIdTemplate] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async createOidcCredentialOffer( + createOidcCredentialOffer: CreateOidcCredentialOffer, + orgId: string, + userDetails: user, + issuerId: string + ): Promise { + try { + const filterTemplateIds = extractTemplateIds(createOidcCredentialOffer); + if (!filterTemplateIds) { + throw new BadRequestException('Please provide a valid id'); + } + const getAllOfferTemplates = await this.issuanceRepository.getTemplateByIds(filterTemplateIds, issuerId); + if (!getAllOfferTemplates) { + throw new NotFoundException('No templates found for the issuer'); + } + + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + //TDOD: signerOption should be under credentials change this with x509 support + const signerOption: SignerOption = { + method: SignerMethodOption.DID, + did: agentDetails.orgDid + }; + const buildOidcCredentialOffer: CredentialOfferPayload = buildCredentialOfferPayload( + createOidcCredentialOffer, + getAllOfferTemplates, + signerOption + ); + if (!buildOidcCredentialOffer) { + throw new BadRequestException('Error while creating oidc credential offer'); + } + const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!issuerDetails) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + } + buildOidcCredentialOffer.publicIssuerId = issuerDetails.publicIssuerId; + const url = await getAgentUrl( + await this.getAgentEndpoint(orgId), + CommonConstants.OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER, + issuerDetails.publicIssuerId + ); + + const createCredentialOfferOnAgent = await this._oidcCreateCredentialOffer(buildOidcCredentialOffer, url, orgId); + if (!createCredentialOfferOnAgent) { + throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errroCreateOffer); + } + + return createCredentialOfferOnAgent.response; + } catch (error) { + this.logger.error(`[createOidcCredentialOffer] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + async buildOidcIssuerConfig(issuerId: string) { + try { + const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + const templates = await this.issuanceRepository.getTemplatesByIssuerId(issuerId); + + const credentialConfigurationsSupported = buildCredentialConfigurationsSupported(templates); + + return buildIssuerPayload(credentialConfigurationsSupported, issuerDetails); + } catch (error) { + this.logger.error(`[buildOidcIssuerPayload] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async findAllTemplate(orgId: string, userDetails: user, issuerId: string): Promise { + try { + const templates = await this.issuanceRepository.getTemplatesByIssuerId(issuerId); + return { message: ResponseMessages.oidcTemplate.success.fetch, data: templates }; + } catch (error) { + this.logger.error(`[findAllTemplate] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async _createOIDCIssuer(issuerCreation, url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'agent-create-oidc-issuer' }; + const payload: IAgentOIDCIssuerCreate = { issuerCreation, url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_createOIDCIssuer] [NATS call]- error in create OIDC Issuer : ${JSON.stringify(error)}`); + throw error; + } + } + + async _createOIDCTemplate(templatePayload, url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'agent-create-oidc-template' }; + const payload = { templatePayload, url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_createOIDCTemplate] [NATS call]- error in create OIDC Template : ${JSON.stringify(error)}`); + throw error; + } + } + + async _deleteOidcIssuer(url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'delete-oidc-issuer' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_createOIDCTemplate] [NATS call]- error in create OIDC Template : ${JSON.stringify(error)}`); + throw error; + } + } + + async _oidcGetIssuerById(url: string, orgId: string) { + try { + const pattern = { cmd: 'oidc-get-issuer-by-id' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_oidcGetIssuerById] [NATS call]- error in oidc get issuer by id : ${JSON.stringify(error)}`); + throw error; + } + } + + async _oidcGetIssuers(url: string, orgId: string) { + try { + const pattern = { cmd: 'oidc-get-issuers' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_oidcGetIssuers] [NATS call]- error in oidc get issuers : ${JSON.stringify(error)}`); + throw error; + } + } + + async _oidcCreateCredentialOffer(credentialPayload: CredentialOfferPayload, url: string, orgId: string) { + try { + const pattern = { cmd: 'agent-service-oidc-create-credential-offer' }; + const payload = { credentialPayload, url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_oidcCreateCredentialOffer] [NATS call]- error in oidc create credential offer : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async natsCall( + pattern: object, + payload: object + ): Promise<{ + response: string; + }> { + try { + return this.issuanceServiceProxy + .send(pattern, payload) + .pipe( + map((response) => ({ + response + })) + ) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.statusCode, + error: error.message + }, + error.error + ); + }); + } catch (error) { + this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); + throw error; + } + } + + async getAgentEndpoint(orgId: string): Promise { + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + if (!agentDetails.agentEndPoint || '' === agentDetails.agentEndPoint.trim()) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + return agentDetails.agentEndPoint; + } +} diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 7de2755a3..be75ccede 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -117,6 +117,13 @@ export enum CommonConstants { // CREATE KEYS CREATE_POLYGON_SECP256k1_KEY = '/polygon/create-keys', + // OIDC URLs + URL_OIDC_ISSUER_CREATE = '/openid4vc/issuer', + /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase, @typescript-eslint/no-duplicate-enum-values */ + URL_OIDC_GET_ISSUES = '/openid4vc/issuer', + URL_OIDC_ISSUER_UPDATE = '/openid4vc/issuer/#', + URL_OIDC_ISSUER_SESSIONS_CREATE = '/openid4vc/issuance-sessions/create-credential-offer', + // Nested attribute separator NESTED_ATTRIBUTE_SEPARATOR = '~', @@ -383,7 +390,15 @@ export enum CommonConstants { GET_VERIFIED_PROOF = 'get-verified-proof', GET_QUESTION_ANSWER_RECORD = 'get-question-answer-record', SEND_QUESTION = 'send-question', - SEND_BASIC_MESSAGE = 'send-basic-message' + SEND_BASIC_MESSAGE = 'send-basic-message', + + // OIDC + OIDC_ISSUER_CREATE = 'create-oidc-issuer', + OIDC_ISSUER_DELETE = 'delete-oidc-issuer', + OIDC_GET_ALL_ISSUERS = 'get-all-oidc-issuers', + OIDC_ISSUER_BY_ID = 'get-issuer-by-id', + OIDC_ISSUER_TEMPLATE = 'create-oidc-template', + OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER = 'create-oidc-credential-offer' } export const MICRO_SERVICE_NAME = Symbol('MICRO_SERVICE_NAME'); export const ATTRIBUTE_NAME_REGEX = /\['(.*?)'\]/; diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index 938e795c3..b1c70e012 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -93,7 +93,16 @@ export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramI [String(CommonConstants.GET_VERIFIED_PROOF), String(CommonConstants.URL_PROOF_FORM_DATA)], [String(CommonConstants.GET_QUESTION_ANSWER_RECORD), String(CommonConstants.URL_QUESTION_ANSWER_RECORD)], [String(CommonConstants.SEND_QUESTION), String(CommonConstants.URL_SEND_QUESTION)], - [String(CommonConstants.SEND_BASIC_MESSAGE), String(CommonConstants.URL_SEND_BASIC_MESSAGE)] + [String(CommonConstants.SEND_BASIC_MESSAGE), String(CommonConstants.URL_SEND_BASIC_MESSAGE)], + [String(CommonConstants.OIDC_ISSUER_CREATE), String(CommonConstants.URL_OIDC_ISSUER_CREATE)], + [String(CommonConstants.OIDC_GET_ALL_ISSUERS), String(CommonConstants.URL_OIDC_GET_ISSUES)], + [String(CommonConstants.OIDC_ISSUER_DELETE), String(CommonConstants.URL_OIDC_ISSUER_UPDATE)], + [String(CommonConstants.OIDC_ISSUER_BY_ID), String(CommonConstants.URL_OIDC_ISSUER_UPDATE)], + [String(CommonConstants.OIDC_ISSUER_TEMPLATE), String(CommonConstants.URL_OIDC_ISSUER_UPDATE)], + [ + String(CommonConstants.OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER), + String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_CREATE) + ] ]); const urlSuffix = agentUrlMap.get(urlFlag); diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index bd8f5f6fd..11d53a256 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -503,6 +503,54 @@ export const ResponseMessages = { walletRecordNotFound: 'Wallet record not found.' } }, + oidcIssuer: { + success: { + create: 'OIDC issuer created successfully.', + update: 'OIDC issuer updated successfully.', + delete: 'OIDC issuer deleted successfully.', + fetch: 'OIDC issuer(s) fetched successfully.', + issuerConfig: 'Issuer config details created successfully', + issuerConfigUpdate: 'Issuer config details updated successfully' + }, + error: { + notFound: 'OIDC issuer not found.', + invalidId: 'Invalid OIDC issuer ID.', + createFailed: 'Failed to create OIDC issuer.', + updateFailed: 'Failed to update OIDC issuer.', + deleteFailed: 'Failed to delete OIDC issuer.' + } + }, + oidcTemplate: { + success: { + create: 'OIDC template created successfully.', + update: 'OIDC template updated successfully.', + delete: 'OIDC template deleted successfully.', + fetch: 'OIDC template(s) fetched successfully.' + }, + error: { + notFound: 'OIDC template not found.', + invalidId: 'Invalid OIDC template ID.', + createFailed: 'Failed to create OIDC template.', + updateFailed: 'Failed to update OIDC template.', + deleteFailed: 'Failed to delete OIDC template.', + issuerDisplayNotFound: 'Issuer display not found.', + issuerDetailsNotFound: 'Issuer details not found.', + templateNameAlreadyExist: 'Template name already exists for this issuer.', + deleteTemplate: 'Error while deleting template.' + } + }, + oidcIssuerSession: { + success: { + create: 'OIDC Credential offer created successfully.' + }, + error: { + errroCreateOffer: 'Error while creating OIDC credential offer on agent.', + invalidId: 'Invalid OIDC issuer ID.', + createFailed: 'Failed to create OIDC issuer.', + updateFailed: 'Failed to update OIDC issuer.', + deleteFailed: 'Failed to delete OIDC issuer.' + } + }, nats: { success: {}, error: { diff --git a/libs/prisma-service/prisma/migrations/20250814141522_add_supported_protocol/migration.sql b/libs/prisma-service/prisma/migrations/20250814141522_add_supported_protocol/migration.sql new file mode 100644 index 000000000..1c42976ca --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250814141522_add_supported_protocol/migration.sql @@ -0,0 +1,94 @@ +-- Create enum type +CREATE TYPE "CredentialExchangeProtocol" AS ENUM ('OIDC', 'DIDCOMM'); + +-- Add column +ALTER TABLE "organisation" +ADD COLUMN "supported_protocol" "CredentialExchangeProtocol"[]; + +-- Set default for new rows +ALTER TABLE "organisation" +ALTER COLUMN "supported_protocol" +SET DEFAULT ARRAY['DIDCOMM']::"CredentialExchangeProtocol"[]; + +-- Clean existing duplicates before constraint +-- UPDATE "organisation" +-- SET "supported_protocol" = ARRAY( +-- SELECT DISTINCT unnest("supported_protocol") +-- ) +-- WHERE "supported_protocol" IS NOT NULL; + +-- Backfill missing/empty with DIDCOMM +UPDATE "organisation" +SET "supported_protocol" = ARRAY['DIDCOMM']::"CredentialExchangeProtocol"[] +WHERE "supported_protocol" IS NULL OR cardinality("supported_protocol") = 0; + +-- Add no-duplicates constraint +-- ALTER TABLE "organisation" +-- ADD CONSTRAINT supported_protocol_unique +-- CHECK ( +-- cardinality(supported_protocol) = cardinality( +-- ARRAY(SELECT DISTINCT unnest(supported_protocol)) +-- ) +-- ); +-- trigger function to check unique values +CREATE OR REPLACE FUNCTION check_unique_protocols() +RETURNS trigger AS $$ +BEGIN + IF (SELECT COUNT(*) FROM unnest(NEW.supported_protocol) v + GROUP BY v HAVING COUNT(*) > 1 LIMIT 1) IS NOT NULL THEN + RAISE EXCEPTION 'supported_protocol contains duplicates'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER enforce_unique_protocols +BEFORE INSERT OR UPDATE ON organisation +FOR EACH ROW EXECUTE FUNCTION check_unique_protocols(); + + + +-- contraints, triggers and migrations for ledger table +ALTER TABLE "ledgers" +ADD COLUMN "supported_protocol" "CredentialExchangeProtocol"[]; + +-- Set default for new rows +ALTER TABLE "ledgers" +ALTER COLUMN "supported_protocol" +SET DEFAULT ARRAY['DIDCOMM']::"CredentialExchangeProtocol"[]; + +-- Clean existing duplicates before constraint +-- UPDATE "ledgers" +-- SET "supported_protocol" = ARRAY( +-- SELECT DISTINCT unnest("supported_protocol") +-- ) +-- WHERE "supported_protocol" IS NOT NULL; + +-- Backfill missing/empty with DIDCOMM +UPDATE "ledgers" +SET "supported_protocol" = ARRAY['DIDCOMM']::"CredentialExchangeProtocol"[] +WHERE "supported_protocol" IS NULL OR cardinality("supported_protocol") = 0; + +-- Add no-duplicates constraint +-- ALTER TABLE "ledgers" +-- ADD CONSTRAINT supported_protocol_unique +-- CHECK ( +-- cardinality(supported_protocol) = cardinality( +-- ARRAY(SELECT DISTINCT unnest(supported_protocol)) +-- ) +-- ); +-- trigger function to check unique values +CREATE OR REPLACE FUNCTION check_unique_protocols() +RETURNS trigger AS $$ +BEGIN + IF (SELECT COUNT(*) FROM unnest(NEW.supported_protocol) v + GROUP BY v HAVING COUNT(*) > 1 LIMIT 1) IS NOT NULL THEN + RAISE EXCEPTION 'supported_protocol contains duplicates'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER enforce_unique_protocols +BEFORE INSERT OR UPDATE ON ledgers +FOR EACH ROW EXECUTE FUNCTION check_unique_protocols(); \ No newline at end of file diff --git a/libs/prisma-service/prisma/migrations/20250822104325_oidc_issuer_templates/migration.sql b/libs/prisma-service/prisma/migrations/20250822104325_oidc_issuer_templates/migration.sql new file mode 100644 index 000000000..4118c6b20 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250822104325_oidc_issuer_templates/migration.sql @@ -0,0 +1,17 @@ +-- Drop the table if it exists (safe for dev) +DROP TABLE IF EXISTS "public"."credential_templates"; + +-- Create table +CREATE TABLE "public"."credential_templates" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "format" TEXT NOT NULL, + "issuer" TEXT NOT NULL, + "canBeRevoked" BOOLEAN NOT NULL DEFAULT false, + "attributes" JSON NOT NULL, + "appearance" JSON NOT NULL, + "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMPTZ(6) NOT NULL DEFAULT now(), + CONSTRAINT "credential_templates_pkey" PRIMARY KEY ("id") +); diff --git a/libs/prisma-service/prisma/migrations/20250822145801_fix_issuer_relation/migration.sql b/libs/prisma-service/prisma/migrations/20250822145801_fix_issuer_relation/migration.sql new file mode 100644 index 000000000..643f2dbf2 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250822145801_fix_issuer_relation/migration.sql @@ -0,0 +1,32 @@ +/* + Warnings: + + - You are about to drop the column `issuer` on the `credential_templates` table. All the data in the column will be lost. + - Added the required column `issuerId` to the `credential_templates` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "credential_templates" DROP COLUMN "issuer", +ADD COLUMN "issuerId" UUID NOT NULL, +ALTER COLUMN "attributes" SET DATA TYPE JSONB, +ALTER COLUMN "appearance" SET DATA TYPE JSONB, +ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMP(3), +ALTER COLUMN "updatedAt" DROP DEFAULT, +ALTER COLUMN "updatedAt" SET DATA TYPE TIMESTAMP(3); + +-- CreateTable +CREATE TABLE "oidc_issuer" ( + "id" UUID NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" UUID NOT NULL, + "agentIssuerId" TEXT NOT NULL, + "metadata" JSONB NOT NULL, + + CONSTRAINT "oidc_issuer_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "oidc_issuer" ADD CONSTRAINT "oidc_issuer_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "org_agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "credential_templates" ADD CONSTRAINT "credential_templates_issuerId_fkey" FOREIGN KEY ("issuerId") REFERENCES "oidc_issuer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20250902082321_add_org_agent_id_to_oidc_issuer/migration.sql b/libs/prisma-service/prisma/migrations/20250902082321_add_org_agent_id_to_oidc_issuer/migration.sql new file mode 100644 index 000000000..b39a42c37 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250902082321_add_org_agent_id_to_oidc_issuer/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - You are about to drop the column `agentIssuerId` on the `oidc_issuer` table. All the data in the column will be lost. + - Added the required column `orgAgentId` to the `oidc_issuer` table without a default value. This is not possible if the table is not empty. + - Added the required column `publicIssuerId` to the `oidc_issuer` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "oidc_issuer" DROP CONSTRAINT "oidc_issuer_createdBy_fkey"; + +-- AlterTable +ALTER TABLE "oidc_issuer" DROP COLUMN "agentIssuerId", +ADD COLUMN "orgAgentId" UUID NOT NULL, +ADD COLUMN "publicIssuerId" TEXT NOT NULL; + +-- CreateIndex +CREATE INDEX "oidc_issuer_orgAgentId_idx" ON "oidc_issuer"("orgAgentId"); + +-- AddForeignKey +ALTER TABLE "oidc_issuer" ADD CONSTRAINT "oidc_issuer_orgAgentId_fkey" FOREIGN KEY ("orgAgentId") REFERENCES "org_agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20250904181430_added_fields_in_oidc_issuer/migration.sql b/libs/prisma-service/prisma/migrations/20250904181430_added_fields_in_oidc_issuer/migration.sql new file mode 100644 index 000000000..f8517c3c7 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250904181430_added_fields_in_oidc_issuer/migration.sql @@ -0,0 +1,6 @@ +-- CreateEnum +CREATE TYPE "AccessTokenSignerKeyType" AS ENUM ('ed25519'); + +-- AlterTable +ALTER TABLE "oidc_issuer" ADD COLUMN "accessTokenSignerKeyType" "AccessTokenSignerKeyType"[] DEFAULT ARRAY['ed25519']::"AccessTokenSignerKeyType"[], +ADD COLUMN "batchCredentialIssuanceSize" INTEGER NOT NULL DEFAULT 0; diff --git a/libs/prisma-service/prisma/migrations/20250911102632_added_field_signer_option_in_credential_templates/migration.sql b/libs/prisma-service/prisma/migrations/20250911102632_added_field_signer_option_in_credential_templates/migration.sql new file mode 100644 index 000000000..58d8d3ebf --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250911102632_added_field_signer_option_in_credential_templates/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - You are about to drop the column `accessTokenSignerKeyType` on the `oidc_issuer` table. All the data in the column will be lost. + - Added the required column `signerOption` to the `credential_templates` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "SignerOption" AS ENUM ('did', 'x509'); + +-- AlterTable +ALTER TABLE "credential_templates" ADD COLUMN "signerOption" "SignerOption" NOT NULL; + +-- AlterTable +ALTER TABLE "oidc_issuer" DROP COLUMN "accessTokenSignerKeyType"; + +-- DropEnum +DROP TYPE "AccessTokenSignerKeyType"; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index dfba4e1f4..05fada62d 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -229,6 +229,7 @@ model org_agents { organisation organisation? @relation(fields: [orgId], references: [id]) webhookUrl String? @db.VarChar org_dids org_dids[] + oidc_issuer oidc_issuer[] } model org_dids { @@ -568,3 +569,39 @@ model client_aliases { clientAlias String? clientUrl String } + +model oidc_issuer { + id String @id @default(uuid()) @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + publicIssuerId String + metadata Json + orgAgentId String @db.Uuid + orgAgent org_agents @relation(fields: [orgAgentId], references: [id]) + templates credential_templates[] + batchCredentialIssuanceSize Int @default(0) + @@index([orgAgentId]) +} + +enum SignerOption { + did + x509 +} + +model credential_templates { + id String @id @default(uuid()) + name String + description String? + format String // e.g. "sd_jwt", "mso_mdoc" + canBeRevoked Boolean @default(false) + + attributes Json + appearance Json + + issuerId String @db.Uuid + issuer oidc_issuer @relation(fields: [issuerId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + signerOption SignerOption +} \ No newline at end of file From 1e07fe32ad7f1f24e8c5246b808b8d29b682096d Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Tue, 7 Oct 2025 12:00:16 +0530 Subject: [PATCH 02/43] feat: oid4vc issuance (#1455) * feat: oidc issunace Signed-off-by: Tipu_Singh * feat:create seperate microservice for oid4vc Signed-off-by: Tipu_Singh * feat:removed logs Signed-off-by: Tipu_Singh * refactor: removed duplicate code Signed-off-by: Tipu_Singh * feat: create credential offer API Signed-off-by: Tipu_Singh * fix: missing credential details repository logic Signed-off-by: Tipu_Singh * feat: added docker file for oid4vc-issuance Signed-off-by: Tipu_Singh --------- Signed-off-by: Tipu_Singh --- Dockerfiles/Dockerfile.oid4vc-issuance | 45 ++ .../src/agent-service.controller.ts | 41 +- .../src/agent-service.service.ts | 64 ++- apps/api-gateway/src/app.module.ts | 4 +- .../src/issuance/dtos/issuer-sessions.dto.ts | 224 -------- .../src/issuance/issuance.controller.ts | 1 - .../src/issuance/issuance.module.ts | 3 +- .../src/issuance/issuance.service.ts | 89 +--- .../dtos/issuer-sessions.dto.ts | 380 ++++++++++++++ .../dtos/oid4vc-credential-wh.dto.ts | 50 ++ .../dtos/oid4vc-issuer-template.dto.ts} | 37 +- .../dtos/oid4vc-issuer.dto.ts} | 14 +- .../oid4vc-issuance.controller.ts} | 330 ++++++++---- .../oid4vc-issuance/oid4vc-issuance.module.ts | 24 + .../oid4vc-issuance.service.ts | 138 +++++ apps/issuance/constant/issuance.ts | 2 +- apps/issuance/src/issuance.controller.ts | 123 +---- apps/issuance/src/issuance.module.ts | 2 - apps/issuance/src/issuance.repository.ts | 185 ------- apps/oid4vc-issuance/constant/issuance.ts | 6 + .../interfaces/oid4vc-issuance.interfaces.ts} | 19 + .../oid4vc-issuer-sessions.interfaces.ts} | 18 +- .../interfaces/oid4vc-template.interfaces.ts} | 4 +- .../interfaces/oid4vc-wh-interfaces.ts | 48 ++ .../helpers/credential-sessions.builder.ts | 94 +++- .../libs/helpers/issuer.metadata.ts | 41 +- apps/oid4vc-issuance/src/main.ts | 23 + .../src/oid4vc-issuance.controller.ts | 182 +++++++ .../src/oid4vc-issuance.module.ts | 36 ++ .../src/oid4vc-issuance.repository.ts | 278 ++++++++++ .../src/oid4vc-issuance.service.ts} | 479 ++++++++++++++---- apps/oid4vc-issuance/test/app.e2e-spec.ts | 19 + apps/oid4vc-issuance/test/jest-e2e.json | 9 + apps/oid4vc-issuance/tsconfig.app.json | 9 + libs/common/src/common.constant.ts | 21 +- libs/common/src/common.utils.ts | 6 +- libs/common/src/nats.config.ts | 4 +- libs/common/src/response-messages/index.ts | 51 +- libs/enum/src/enum.ts | 13 + libs/prisma-service/prisma/schema.prisma | 19 +- nest-cli.json | 11 +- package.json | 2 +- tsconfig.json | 3 +- 43 files changed, 2247 insertions(+), 904 deletions(-) create mode 100644 Dockerfiles/Dockerfile.oid4vc-issuance delete mode 100644 apps/api-gateway/src/issuance/dtos/issuer-sessions.dto.ts create mode 100644 apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts create mode 100644 apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts rename apps/api-gateway/src/{issuance/dtos/oidc-issuer-template.dto.ts => oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts} (77%) rename apps/api-gateway/src/{issuance/dtos/oidc-issuer.dto.ts => oid4vc-issuance/dtos/oid4vc-issuer.dto.ts} (94%) rename apps/api-gateway/src/{issuance/oidc.controller.ts => oid4vc-issuance/oid4vc-issuance.controller.ts} (53%) create mode 100644 apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts create mode 100644 apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts create mode 100644 apps/oid4vc-issuance/constant/issuance.ts rename apps/{issuance/interfaces/oidc-issuance.interfaces.ts => oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts} (89%) rename apps/{issuance/interfaces/oidc-issuer-sessions.interfaces.ts => oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts} (77%) rename apps/{issuance/interfaces/oidc-template.interface.ts => oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts} (86%) create mode 100644 apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts rename apps/{issuance => oid4vc-issuance}/libs/helpers/credential-sessions.builder.ts (72%) rename apps/{issuance => oid4vc-issuance}/libs/helpers/issuer.metadata.ts (84%) create mode 100644 apps/oid4vc-issuance/src/main.ts create mode 100644 apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts create mode 100644 apps/oid4vc-issuance/src/oid4vc-issuance.module.ts create mode 100644 apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts rename apps/{issuance/src/oidc-issuance.service.ts => oid4vc-issuance/src/oid4vc-issuance.service.ts} (51%) create mode 100644 apps/oid4vc-issuance/test/app.e2e-spec.ts create mode 100644 apps/oid4vc-issuance/test/jest-e2e.json create mode 100644 apps/oid4vc-issuance/tsconfig.app.json diff --git a/Dockerfiles/Dockerfile.oid4vc-issuance b/Dockerfiles/Dockerfile.oid4vc-issuance new file mode 100644 index 000000000..d127d2e9a --- /dev/null +++ b/Dockerfiles/Dockerfile.oid4vc-issuance @@ -0,0 +1,45 @@ +# Stage 1: Build the application +FROM node:18-alpine as build +# Install OpenSSL +RUN apk add --no-cache openssl +RUN npm install -g pnpm +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +#COPY package-lock.json ./ + +ENV PUPPETEER_SKIP_DOWNLOAD=true + +# Install dependencies while ignoring scripts (including Puppeteer's installation) +RUN pnpm i --ignore-scripts + +# Copy the rest of the application code +COPY . . +# RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate +RUN cd libs/prisma-service && npx prisma generate + +# Build the issuance service +RUN pnpm run build issuance + +# Stage 2: Create the final image +FROM node:18-alpine +# Install OpenSSL +RUN apk add --no-cache openssl +# RUN npm install -g pnpm +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/oid4vc-issuance/ ./dist/apps/oid4vc-issuance/ + +# Copy the libs folder from the build stage +COPY --from=build /app/libs/ ./libs/ +#COPY --from=build /app/package.json ./ +COPY --from=build /app/node_modules ./node_modules +# COPY --from=build /app/uploadedFiles ./uploadedFiles + + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/oid4vc-issuance/main.js"] diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index dcd3cf149..0c4d7c81b 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -324,37 +324,60 @@ export class AgentServiceController { return this.agentServiceService.getAgentDetails(payload.orgId); } - @MessagePattern({ cmd: 'agent-create-oidc-issuer' }) + @MessagePattern({ cmd: 'agent-create-oid4vc-issuer' }) // eslint-disable-next-line @typescript-eslint/no-explicit-any async oidcIssuerCreate(payload: { issuerCreation; url: string; orgId: string }): Promise { return this.agentServiceService.oidcIssuerCreate(payload.issuerCreation, payload.url, payload.orgId); } - @MessagePattern({ cmd: 'delete-oidc-issuer' }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async oidcDeleteIssuer(payload: { url: string; orgId: string }): Promise { + @MessagePattern({ cmd: 'delete-oid4vc-issuer' }) + async oidcDeleteIssuer(payload: { url: string; orgId: string }): Promise { return this.agentServiceService.deleteOidcIssuer(payload.url, payload.orgId); } - @MessagePattern({ cmd: 'agent-create-oidc-template' }) + @MessagePattern({ cmd: 'agent-create-oid4vc-template' }) // eslint-disable-next-line @typescript-eslint/no-explicit-any async oidcIssuerTemplate(payload: { templatePayload; url: string; orgId: string }): Promise { return this.agentServiceService.oidcIssuerTemplate(payload.templatePayload, payload.url, payload.orgId); } - //TODO: change message for oidc - @MessagePattern({ cmd: 'oidc-get-issuer-by-id' }) + //TODO: change message for oid4vc + @MessagePattern({ cmd: 'oid4vc-get-issuer-by-id' }) // eslint-disable-next-line @typescript-eslint/no-explicit-any async oidcGetIssuerById(payload: { url: string; orgId: string }): Promise { return this.agentServiceService.oidcGetIssuerById(payload.url, payload.orgId); } - @MessagePattern({ cmd: 'oidc-get-issuers' }) + @MessagePattern({ cmd: 'oid4vc-get-issuers-agent-service' }) // eslint-disable-next-line @typescript-eslint/no-explicit-any async oidcGetIssuers(payload: { url: string; orgId: string }): Promise { return this.agentServiceService.oidcGetIssuers(payload.url, payload.orgId); } - @MessagePattern({ cmd: 'agent-service-oidc-create-credential-offer' }) + @MessagePattern({ cmd: 'agent-service-oid4vc-create-credential-offer' }) // eslint-disable-next-line @typescript-eslint/no-explicit-any async oidcCreateCredentialOffer(payload: { credentialPayload; url: string; orgId: string }): Promise { return this.agentServiceService.oidcCreateCredentialOffer(payload.credentialPayload, payload.url, payload.orgId); } + + @MessagePattern({ cmd: 'agent-service-oid4vc-update-credential-offer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcUpdateCredentialOffer(payload: { issuanceMetadata; url: string; orgId: string }): Promise { + return this.agentServiceService.oidcUpdateCredentialOffer(payload.issuanceMetadata, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-service-oid4vc-get-credential-offer-by-id' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcGetCredentialOfferById(payload: { url: string; orgId: string; offerId: string }): Promise { + return this.agentServiceService.oidcGetCredentialOfferById(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-service-oid4vc-get-all-credential-offers' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcGetAllCredentialOffers(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.oidcGetAllCredentialOffers(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-service-oid4vc-delete-credential-offer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async oidcDeleteCredentialOffer(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.oidcDeleteCredentialOffer(payload.url, payload.orgId); + } } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index c7d32a30b..e9c0c08ad 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -1425,13 +1425,15 @@ export class AgentServiceService { } } - async deleteOidcIssuer(url: string, orgId: string): Promise { + async deleteOidcIssuer(url: string, orgId: string): Promise { try { const getApiKey = await this.getOrgAgentApiKey(orgId); - const data = await this.commonService - .httpDelete(url, { headers: { authorization: getApiKey } }) - .then(async (response) => response); - return data; + const response = await this.commonService.httpDelete(url, { + headers: { authorization: getApiKey } + }); + if (response?.status === 204) { + return 'Data deleted successfully'; + } } catch (error) { this.logger.error(`Error in deleteOidcIssuer in agent service : ${JSON.stringify(error)}`); throw error; @@ -1477,6 +1479,58 @@ export class AgentServiceService { } } + async oidcUpdateCredentialOffer(issuanceMetadata, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpPut(url, issuanceMetadata, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in _oidcUpdateCredentialOffer in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcGetCredentialOfferById(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in _oidcGetCredentialOfferById in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcGetAllCredentialOffers(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpGet(url, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in _oidcGetAllCredentialOffers in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async oidcDeleteCredentialOffer(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const data = await this.commonService + .httpDelete(`${url}`, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return data; + } catch (error) { + this.logger.error(`Error in _oidcDeleteCredentialOffer in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + async oidcIssuerTemplate(templatePayload, url: string, orgId: string): Promise { try { const getApiKey = await this.getOrgAgentApiKey(orgId); diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index b1af54f4d..287ac36ec 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -30,6 +30,7 @@ import { ContextModule } from '@credebl/context/contextModule'; import { LoggerModule } from '@credebl/logger/logger.module'; import { GlobalConfigModule } from '@credebl/config/global-config.module'; import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; +import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; @Module({ imports: [ @@ -62,7 +63,8 @@ import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; GlobalConfigModule, CacheModule.register(), GeoLocationModule, - CloudWalletModule + CloudWalletModule, + Oid4vcIssuanceModule ], controllers: [AppController], providers: [ diff --git a/apps/api-gateway/src/issuance/dtos/issuer-sessions.dto.ts b/apps/api-gateway/src/issuance/dtos/issuer-sessions.dto.ts deleted file mode 100644 index a31c65799..000000000 --- a/apps/api-gateway/src/issuance/dtos/issuer-sessions.dto.ts +++ /dev/null @@ -1,224 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ - -import { - IsArray, - IsEnum, - IsNotEmpty, - IsObject, - IsOptional, - IsString, - Matches, - ValidateNested, - registerDecorator, - ValidationOptions, - IsInt, - Min, - IsIn, - ArrayMinSize, - IsUrl, - ValidatorConstraint, - ValidatorConstraintInterface, - ValidationArguments, - Validate -} from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; - -export enum CredentialFormat { - SdJwtVc = 'vc+sd-jwt', - Mdoc = 'mdoc' -} - -export enum SignerMethodOption { - DID = 'did', - X5C = 'x5c' -} - -/** ---------- custom validator: disclosureFrame ---------- */ -function isDisclosureFrameValue(v: unknown): boolean { - if ('boolean' === typeof v) { - return true; - } - if (v && 'object' === typeof v && !Array.isArray(v)) { - return Object.values(v as Record).every((x) => 'boolean' === typeof x); - } - return false; -} - -export function IsDisclosureFrame(options?: ValidationOptions) { - return function (object: unknown, propertyName: string) { - registerDecorator({ - name: 'IsDisclosureFrame', - target: (object as object).constructor, - propertyName, - options, - validator: { - validate(value: unknown) { - if (value === undefined) { - return true; - } - if (!value || 'object' !== typeof value || Array.isArray(value)) { - return false; - } - return Object.values(value as Record).every(isDisclosureFrameValue); - }, - defaultMessage() { - return 'disclosureFrame must be a map of booleans or nested maps of booleans'; - } - } - }); - }; -} - -/** ---------- payload DTOs ---------- */ -export class CredentialPayloadDto { - @ApiPropertyOptional() - @IsOptional() - @IsString() - vct?: string; - - @ApiPropertyOptional({ example: 'Garry' }) - @IsOptional() - @IsString() - full_name?: string; - - @ApiPropertyOptional({ example: '2000-01-01', description: 'YYYY-MM-DD' }) - @IsOptional() - @Matches(/^\d{4}-\d{2}-\d{2}$/, { message: 'birth_date must be YYYY-MM-DD' }) - birth_date?: string; - - @ApiPropertyOptional({ example: 'Africa' }) - @IsOptional() - @IsString() - birth_place?: string; - - @ApiPropertyOptional({ example: 'James Bear' }) - @IsOptional() - @IsString() - parent_names?: string; - - [key: string]: unknown; -} - -export class CredentialRequestDto { - @ApiProperty({ example: '1b2d3c4e-...' }) - @IsString() - @IsNotEmpty() - templateId!: string; - - @ApiProperty({ enum: CredentialFormat, example: CredentialFormat.SdJwtVc }) - @IsEnum(CredentialFormat) - format!: CredentialFormat; - - @ApiProperty({ - type: CredentialPayloadDto, - description: 'Credential payload (structure depends on the format)' - }) - @ValidateNested() - @Type(() => CredentialPayloadDto) - payload!: CredentialPayloadDto; - - @ApiPropertyOptional({ - description: 'Selective disclosure frame (claim -> boolean or nested map).', - example: { full_name: true, birth_date: true, birth_place: false, parent_names: false }, - required: false - }) - @IsOptional() - @IsObject() - @IsDisclosureFrame() - disclosureFrame?: Record>; -} - -/** ---------- auth-config DTOs ---------- */ -export class TxCodeDto { - @ApiPropertyOptional({ example: 'test abc' }) - @IsOptional() - @IsString() - description?: string; - - @ApiProperty({ example: 4 }) - @IsInt() - @Min(1) - length!: number; - - @ApiProperty({ example: 'numeric', enum: ['numeric'] }) - @IsString() - @IsIn(['numeric']) - input_mode!: 'numeric'; -} - -export class PreAuthorizedCodeFlowConfigDto { - @ApiProperty({ type: TxCodeDto }) - @ValidateNested() - @Type(() => TxCodeDto) - txCode!: TxCodeDto; - - @ApiProperty({ - example: 'http://localhost:4001/oid4vci/abc-gov', - description: 'AS (Authorization Server) base URL' - }) - @IsUrl({ require_tld: false }) - authorizationServerUrl!: string; -} - -export class AuthorizationCodeFlowConfigDto { - @ApiProperty({ - example: 'https://id.credebl.ae:8443/realms/credebl', - description: 'AS (Authorization Server) base URL' - }) - @IsUrl({ require_tld: false }) - authorizationServerUrl!: string; -} - -/** ---------- class-level constraint: EXACTLY ONE of the two configs ---------- */ -@ValidatorConstraint({ name: 'ExactlyOneOf', async: false }) -class ExactlyOneOfConstraint implements ValidatorConstraintInterface { - validate(_: unknown, args: ValidationArguments) { - const obj = args.object as Record; - const keys = (args.constraints ?? []) as string[]; - const present = keys.filter((k) => obj[k] !== undefined && null !== obj[k]); - return 1 === present.length; - } - defaultMessage(args: ValidationArguments) { - const keys = (args.constraints ?? []) as string[]; - return `Exactly one of [${keys.join(', ')}] must be provided (not both, not neither).`; - } -} -function ExactlyOneOf(keys: string[], options?: ValidationOptions) { - return Validate(ExactlyOneOfConstraint, keys, options); -} - -/** ---------- root DTO (no authenticationType) ---------- */ -export class CreateOidcCredentialOfferDto { - @ApiProperty({ - type: [CredentialRequestDto], - description: 'At least one credential to be issued.' - }) - @IsArray() - @ArrayMinSize(1) - @ValidateNested({ each: true }) - @Type(() => CredentialRequestDto) - credentials!: CredentialRequestDto[]; - - // Each is optional individually; XOR rule below enforces exactly one present. - @ApiPropertyOptional({ type: PreAuthorizedCodeFlowConfigDto }) - @IsOptional() - @ValidateNested() - @Type(() => PreAuthorizedCodeFlowConfigDto) - preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfigDto; - - @ApiPropertyOptional({ type: AuthorizationCodeFlowConfigDto }) - @IsOptional() - @ValidateNested() - @Type(() => AuthorizationCodeFlowConfigDto) - authorizationCodeFlowConfig?: AuthorizationCodeFlowConfigDto; - - issuerId?: string; - - // Host the class-level XOR validator on a dummy property - @ExactlyOneOf(['preAuthorizedCodeFlowConfig', 'authorizationCodeFlowConfig'], { - message: 'Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.' - }) - private readonly _exactlyOne?: unknown; -} diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index 86add6131..c6bb03b89 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -946,7 +946,6 @@ export class IssuanceController { @Res() res: Response ): Promise { issueCredentialDto.type = 'Issuance'; - if (id && 'default' === issueCredentialDto.contextCorrelationId) { issueCredentialDto.orgId = id; } diff --git a/apps/api-gateway/src/issuance/issuance.module.ts b/apps/api-gateway/src/issuance/issuance.module.ts index 5a7df978c..d5f184d16 100644 --- a/apps/api-gateway/src/issuance/issuance.module.ts +++ b/apps/api-gateway/src/issuance/issuance.module.ts @@ -8,7 +8,6 @@ import { getNatsOptions } from '@credebl/common/nats.config'; import { AwsService } from '@credebl/aws'; import { CommonConstants } from '@credebl/common/common.constant'; import { NATSClient } from '@credebl/common/NATSClient'; -import { OidcController } from './oidc.controller'; @Module({ imports: [ @@ -21,7 +20,7 @@ import { OidcController } from './oidc.controller'; } ]) ], - controllers: [IssuanceController, OidcController], + controllers: [IssuanceController], providers: [IssuanceService, CommonService, AwsService, NATSClient] }) export class IssuanceModule {} diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index 3b0187823..17e2dfbe9 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -27,9 +27,9 @@ import { IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; import { IssueCredentialDto } from './dtos/multi-connection.dto'; -import { oidc_issuer, user } from '@prisma/client'; import { NATSClient } from '@credebl/common/NATSClient'; import { ClientProxy } from '@nestjs/microservices'; +import { user } from '@prisma/client'; @Injectable() export class IssuanceService extends BaseService { constructor( @@ -272,91 +272,4 @@ export class IssuanceService extends BaseService { }; return this.natsClient.sendNatsMessage(this.issuanceProxy, 'issued-file-data-and-file-details', payload); } - - // OIDC methods - - async oidcIssuerCreate( - issueCredentialDto: IssuerCreationDto, - orgId: string, - userDetails: user - ): Promise { - const payload = { issueCredentialDto, orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-issuer-create', payload); - } - - async oidcIssuerUpdate(issueUpdationDto: IssuerUpdationDto, orgId: string, userDetails: user) { - const payload = { issueUpdationDto, orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-issuer-update', payload); - } - - async oidcGetIssuerById(issuerId: string, orgId: string) { - const payload = { issuerId, orgId }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-issuer-get-by-id', payload); - } - - async oidcGetIssuers(orgId: string) { - const payload = { orgId }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-get-issuers', payload); - } - - async oidcDeleteIssuer(userDetails: user, orgId: string, issuerId: string) { - const payload = { issuerId, orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-delete-issuer', payload); - } - - async deleteTemplate(userDetails: user, orgId: string, templateId: string) { - const payload = { templateId, orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-delete', payload); - } - - async updateTemplate( - userDetails: user, - orgId: string, - templateId: string, - dto: UpdateCredentialTemplateDto, - issuerId: string - ) { - const payload = { templateId, orgId, userDetails, dto, issuerId }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-update', payload); - } - - async findByIdTemplate(userDetails: user, orgId: string, templateId: string) { - const payload = { templateId, orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-find-id', payload); - } - - async findAllTemplate(userDetails: user, orgId: string) { - const payload = { orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-find-all', payload); - } - - async createTemplate( - CredentialTemplate: CreateCredentialTemplateDto, - userDetails: user, - orgId: string, - issuerId: string - ) { - const payload = { CredentialTemplate, orgId, userDetails, issuerId }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-template-create', payload); - } - - async createOidcCredentialOffer( - oidcCredentialPayload: CreateOidcCredentialOfferDto, - userDetails: user, - orgId: string, - issuerId: string - ) { - const payload = { oidcCredentialPayload, orgId, userDetails, issuerId }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oidc-create-credential-offer', payload); - } - - oidcIssueCredentialWebhook( - oidcIssueCredentialDto, - id: string - ): Promise<{ - response: object; - }> { - const payload = { oidcIssueCredentialDto, id }; - return this.natsClient.sendNats(this.issuanceProxy, 'webhook-oidc-issue-credential', payload); - } } 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 new file mode 100644 index 000000000..4d056d2fc --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts @@ -0,0 +1,380 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ +import { + IsArray, + IsNotEmpty, + IsObject, + IsOptional, + IsString, + ValidateNested, + registerDecorator, + ValidationOptions, + ArrayMinSize, + IsInt, + Min, + IsIn, + IsUrl, + ValidatorConstraint, + ValidatorConstraintInterface, + ValidationArguments, + Validate +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; + +/* ========= Enums ========= */ +export enum CredentialFormat { + SdJwtVc = 'vc+sd-jwt', + Mdoc = 'mdoc' +} + +/* ========= disclosureFrame custom validator ========= */ +function isDisclosureFrameValue(v: unknown): boolean { + if ('boolean' === typeof v) { + return true; + } + if (v && 'object' === typeof v && !Array.isArray(v)) { + return Object.values(v as Record).every((x) => 'boolean' === typeof x); + } + return false; +} + +export function IsDisclosureFrame(options?: ValidationOptions) { + return function (object: unknown, propertyName: string) { + registerDecorator({ + name: 'IsDisclosureFrame', + target: (object as object).constructor, + propertyName, + options, + validator: { + validate(value: unknown) { + if (value === undefined) { + return true; + } // optional + if (!value || 'object' !== typeof value || Array.isArray(value)) { + return false; + } + return Object.values(value as Record).every(isDisclosureFrameValue); + }, + defaultMessage() { + return 'disclosureFrame must be a map of booleans or nested maps of booleans'; + } + } + }); + }; +} + +/* ========= Auth flow DTOs ========= */ +export class TxCodeDto { + @ApiPropertyOptional({ example: 'test abc' }) + @IsOptional() + @IsString() + description?: string; + + @ApiProperty({ example: 4 }) + @IsInt() + @Min(1) + length!: number; + + @ApiProperty({ example: 'numeric', enum: ['numeric'] }) + @IsString() + @IsIn(['numeric']) + input_mode!: 'numeric'; +} + +export class PreAuthorizedCodeFlowConfigDto { + @ApiProperty({ type: TxCodeDto }) + @ValidateNested() + @Type(() => TxCodeDto) + txCode!: TxCodeDto; + + @ApiProperty({ + example: 'http://localhost:4001/oid4vci/abc-gov', + description: 'AS (Authorization Server) base URL' + }) + @IsUrl({ require_tld: false }) + authorizationServerUrl!: string; +} + +export class AuthorizationCodeFlowConfigDto { + @ApiProperty({ + example: 'https://id.example.com/realms/issuer', + description: 'AS (Authorization Server) base URL' + }) + @IsUrl({ require_tld: false }) + authorizationServerUrl!: string; +} + +/* ========= XOR class-level validator (exactly one config) ========= */ +@ValidatorConstraint({ name: 'ExactlyOneOf', async: false }) +class ExactlyOneOfConstraint implements ValidatorConstraintInterface { + validate(_: unknown, args: ValidationArguments) { + const obj = args.object as Record; + const keys = (args.constraints ?? []) as string[]; + const present = keys.filter((k) => obj[k] !== undefined && null !== obj[k]); + return 1 === present.length; + } + defaultMessage(args: ValidationArguments) { + const keys = (args.constraints ?? []) as string[]; + return `Exactly one of [${keys.join(', ')}] must be provided (not both, not neither).`; + } +} +function ExactlyOneOf(keys: string[], options?: ValidationOptions) { + return Validate(ExactlyOneOfConstraint, keys, options); +} + +/* ========= Request DTOs ========= */ +export class CredentialRequestDto { + @ApiProperty({ + example: 'c49bdee0-d028-4595-85dc-177c85ea391c', + description: 'Must match credential template id' + }) + @IsString() + @IsNotEmpty() + templateId!: string; + + @ApiProperty({ + description: 'Dynamic claims object', + example: { name: 'Garry', DOB: '2000-01-01', additionalProp3: 'Africa' }, + type: 'object', + additionalProperties: true + }) + @IsObject() + payload!: Record; + + @ApiPropertyOptional({ + description: 'Selective disclosure: claim -> boolean (or nested map)', + example: { name: true, DOB: true, additionalProp3: false }, + required: false + }) + @IsOptional() + @IsDisclosureFrame() + disclosureFrame?: Record>; +} + +export class CreateOidcCredentialOfferDto { + @ApiProperty({ + type: [CredentialRequestDto], + description: 'At least one credential to be issued.' + }) + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => CredentialRequestDto) + credentials!: CredentialRequestDto[]; + + // XOR: exactly one present + @ApiPropertyOptional({ type: PreAuthorizedCodeFlowConfigDto }) + @IsOptional() + @ValidateNested() + @Type(() => PreAuthorizedCodeFlowConfigDto) + preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfigDto; + + @IsOptional() + @ValidateNested() + @Type(() => AuthorizationCodeFlowConfigDto) + authorizationCodeFlowConfig?: AuthorizationCodeFlowConfigDto; + + issuerId?: string; + + // host XOR rule + @ExactlyOneOf(['preAuthorizedCodeFlowConfig', 'authorizationCodeFlowConfig'], { + message: 'Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.' + }) + private readonly _exactlyOne?: unknown; +} + +export class GetAllCredentialOfferDto { + @ApiProperty({ required: false, example: 'credebl university' }) + @IsOptional() + publicIssuerId: string = ''; + + @ApiProperty({ required: false, example: '568345' }) + @IsOptional() + preAuthorizedCode: string = ''; + + // @ApiPropertyOptional({ + // example: OpenId4VcIssuanceSessionState.OfferCreated, + // enum: OpenId4VcIssuanceSessionState, + // required: false, + // }) + // @IsOptional() + // @IsEnum(OpenId4VcIssuanceSessionState) + // state?: OpenId4VcIssuanceSessionState; + + @ApiProperty({ required: false, example: 'openid-credential-offer://?credential_offer_uri=http%3A%2F%2.....' }) + @IsOptional() + credentialOfferUri: string = ''; + + @ApiProperty({ required: false, example: 'Bob' }) + @IsOptional() + authorizationCode: string = ''; +} + +export class UpdateCredentialRequestDto { + @ApiPropertyOptional({ + description: 'Issuer metadata (any valid JSON object)', + type: 'object', + additionalProperties: true + }) + @IsOptional() + @IsObject() + issuerMetadata?: Record; + + issuerId?: string; + + credentialOfferId?: string; +} + +export class SignerOptionsDto { + @IsString() + @IsIn(['did', 'x5c'], { message: 'method must be either "did" or "x5c"' }) + method: string; + + @IsString() + @IsOptional() + did?: string; + + @IsArray() + @IsOptional() + x5c?: string[]; +} + +export class CredentialDto { + @ApiProperty({ + description: 'Unique ID of the supported credential', + example: 'DrivingLicenseCredential-mdoc' + }) + @IsString() + credentialSupportedId: string; + + @ApiProperty({ + description: 'Signer options for credential issuance', + example: { + method: 'x5c', + x5c: [ + 'MIIB3jCCAZCgAwIBAgIQQfBdIK9v3TIzKR+1HjlixDAFBgMrZXAwJDEUMBIGA1UEAxMLRFkgdGVzdCBvcmcxDDAKBgNVBAYTA0lORDAeFw0yNTA5MjQwMDAwMDBaFw0yODA5MjQwMDAwMDBaMCQxFDASBgNVBAMTC0RZIHRlc3Qgb3JnMQwwCgYDVQQGEwNJTkQwKjAFBgMrZXADIQDIkLycOlkHP6+MG4rprj8fyxRfwqhH8Xx9v0XxCd175aOB1zCB1DAdBgNVHQ4EFgQUbqjjbQgbAx3lPjkPBVQwvvF14agwDgYDVR0PAQH/BAQDAgGGMBUGA1UdJQEB/wQLMAkGByiBjF0FAQIwOwYDVR0SBDQwMoIXaHR0cDovL3Rlc3QuZXhhbXBsZS5jb22GF2h0dHA6Ly90ZXN0LmV4YW1wbGUuY29tMDsGA1UdEQQ0MDKCF2h0dHA6Ly90ZXN0LmV4YW1wbGUuY29thhdodHRwOi8vdGVzdC5leGFtcGxlLmNvbTASBgNVHRMBAf8ECDAGAQH/AgEAMAUGAytlcANBALTqC64XSTRUoMmwYbCD/z46U/Je6IeQsh6qq4qXh+wfnMIfJMvLQnG+nMkfeAs3zYAwjK6sCZ/7lHkEJnYObQ4=' + ] + } + }) + @ValidateNested() + @Type(() => SignerOptionsDto) + signerOptions: SignerOptionsDto; + + @ApiProperty({ + description: 'Credential format type', + enum: ['mso_mdoc', 'vc+sd-jwt'], + example: 'mso_mdoc' + }) + @IsString() + @IsIn(['mso_mdoc', 'vc+sd-jwt'], { message: 'format must be either "mso_mdoc" or "vc+sd-jwt"' }) + format: string; + + @ApiProperty({ + description: 'Credential payload (namespace data, validity info, etc.)', + example: { + namespaces: { + 'org.iso.23220.photoID.1': { + birth_date: '1970-02-14', + family_name: 'Müller-Lüdenscheid', + given_name: 'Ford Praxibetel', + document_number: 'LA001801M' + } + }, + validityInfo: { + validFrom: '2025-04-23T14:34:09.188Z', + validUntil: '2026-05-03T14:34:09.188Z' + } + } + }) + @ValidateNested() + payload: object; +} + +export class CreateCredentialOfferD2ADto { + @ApiProperty({ + description: 'Public identifier of the issuer visible to verifiers and wallets.', + example: 'dy-gov' + }) + @IsString() + publicIssuerId: string; + + @ApiProperty({ + description: 'List of credentials to be issued under this offer.', + type: [CredentialDto], + example: [ + { + credentialSupportedId: 'DrivingLicenseCredential-mdoc', + signerOptions: { + method: 'x5c', + x5c: [ + 'MIIB3jCCAZCgAwIBAgIQQfBdIK9v3TIzKR+1HjlixDAFBgMrZXAwJDEUMBIGA1UEAxMLRFkgdGVzdCBvcmcxDDAKBgNVBAYTA0lORDAeFw0yNTA5MjQwMDAwMDBaFw0yODA5MjQwMDAwMDBaMCQxFDASBgNVBAMTC0RZIHRlc3Qgb3JnMQwwCgYDVQQGEwNJTkQwKjAFBgMrZXADIQDIkLycOlkHP6+MG4rprj8fyxRfwqhH8Xx9v0XxCd175aOB1zCB1DAdBgNVHQ4EFgQUbqjjbQgbAx3lPjkPBVQwvvF14agwDgYDVR0PAQH/BAQDAgGGMBUGA1UdJQEB/wQLMAkGByiBjF0FAQIwOwYDVR0SBDQwMoIXaHR0cDovL3Rlc3QuZXhhbXBsZS5jb22GF2h0dHA6Ly90ZXN0LmV4YW1wbGUuY29tMDsGA1UdEQQ0MDKCF2h0dHA6Ly90ZXN0LmV4YW1wbGUuY29thhdodHRwOi8vdGVzdC5leGFtcGxlLmNvbTASBgNVHRMBAf8ECDAGAQH/AgEAMAUGAytlcANBALTqC64XSTRUoMmwYbCD/z46U/Je6IeQsh6qq4qXh+wfnMIfJMvLQnG+nMkfeAs3zYAwjK6sCZ/7lHkEJnYObQ4=' + ] + }, + format: 'mso_mdoc', + payload: { + namespaces: { + 'org.iso.23220.photoID.1': { + birth_date: '1970-02-14', + family_name: 'Müller-Lüdenscheid', + given_name: 'Ford Praxibetel', + document_number: 'LA001801M' + } + }, + validityInfo: { + validFrom: '2025-04-23T14:34:09.188Z', + validUntil: '2026-05-03T14:34:09.188Z' + } + } + } + ] + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CredentialDto) + credentials: CredentialDto[]; + + @ApiPropertyOptional({ + description: 'Pre-Authorized Code Flow configuration. Provide this OR authorizationCodeFlowConfig (XOR rule).', + type: PreAuthorizedCodeFlowConfigDto, + example: { + preAuthorizedCode: 'abcd1234xyz', + txCode: { + length: 8, + inputMode: 'numeric' + }, + userPinRequired: true + } + }) + @IsOptional() + @ValidateNested() + @Type(() => PreAuthorizedCodeFlowConfigDto) + preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfigDto; + + @ApiPropertyOptional({ + description: 'Authorization Code Flow configuration. Provide this OR preAuthorizedCodeFlowConfig (XOR rule).', + type: AuthorizationCodeFlowConfigDto, + example: { + clientId: 'wallet-app', + redirectUri: 'https://wallet.example.org/callback', + scope: 'openid vc_authn', + state: 'xyz-987654321' + } + }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorizationCodeFlowConfigDto) + authorizationCodeFlowConfig?: AuthorizationCodeFlowConfigDto; + + @ApiPropertyOptional({ + description: 'Internal identifier of the issuer (optional, for backend use).', + example: 'issuer-12345' + }) + @IsOptional() + issuerId?: string; + + @ExactlyOneOf(['preAuthorizedCodeFlowConfig', 'authorizationCodeFlowConfig'], { + message: 'Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.' + }) + private readonly _exactlyOne?: unknown; +} diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts new file mode 100644 index 000000000..d4a02f669 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts @@ -0,0 +1,50 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsObject, IsString } from 'class-validator'; + +export class CredentialOfferPayloadDto { + @ApiProperty() + @IsString() + // eslint-disable-next-line camelcase + credential_issuer!: string; + + @ApiProperty({ type: [String] }) + @IsArray() + // eslint-disable-next-line camelcase + credential_configuration_ids!: string[]; + + @ApiProperty({ type: 'object', additionalProperties: true }) + @IsObject() + grants!: Record; + + @ApiProperty({ type: [Object] }) + @IsArray() + credentials!: Record[]; +} + +export class IssuanceMetadataDto { + @ApiProperty() + @IsString() + issuerDid!: string; + + @ApiProperty({ type: [Object] }) + @IsArray() + credentials!: Record[]; +} + +export class OidcIssueCredentialDto { + @ApiProperty() + @IsString() + id!: string; + + @ApiProperty() + @IsString() + credentialOfferId!: string; + + @ApiProperty() + @IsString() + state!: string; + + @ApiProperty() + @IsString() + contextCorrelationId!: string; +} diff --git a/apps/api-gateway/src/issuance/dtos/oidc-issuer-template.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts similarity index 77% rename from apps/api-gateway/src/issuance/dtos/oidc-issuer-template.dto.ts rename to apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts index efd901a89..1035166d7 100644 --- a/apps/api-gateway/src/issuance/dtos/oidc-issuer-template.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts @@ -7,11 +7,13 @@ import { ValidateNested, IsObject, IsNotEmpty, - IsArray + IsArray, + ValidateIf, + IsEmpty } from 'class-validator'; import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath, PartialType } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { DisplayDto } from './oidc-issuer.dto'; +import { DisplayDto } from './oid4vc-issuer.dto'; export class CredentialAttributeDto { @ApiProperty({ required: false, description: 'Whether the attribute is mandatory' }) @@ -125,9 +127,34 @@ export class CreateCredentialTemplateDto { @IsEnum(SignerOption) signerOption!: SignerOption; - @ApiProperty({ enum: ['sd-jwt-vc', 'mdoc'], description: 'Credential format type' }) - @IsEnum(['sd-jwt-vc', 'mso_mdoc', 'vc+sd-jwt']) - format: 'sd-jwt-vc' | 'mso_mdoc ' | 'vc+sd-jwt'; + @ApiProperty({ enum: ['mso_mdoc', 'vc+sd-jwt'], description: 'Credential format type' }) + @IsEnum(['mso_mdoc', 'vc+sd-jwt']) + format: 'mso_mdoc' | 'vc+sd-jwt'; + + @ApiPropertyOptional({ + description: 'Document type (required when format is "mso_mdoc"; must NOT be provided when format is "vc+sd-jwt")', + example: 'org.iso.23220.photoID.1' + }) + @ValidateIf((o: CreateCredentialTemplateDto) => 'mso_mdoc' === o.format) + @IsString() + doctype?: string; + + @ValidateIf((o: CreateCredentialTemplateDto) => 'vc+sd-jwt' === o.format) + @IsEmpty({ message: 'doctype must not be provided when format is "vc+sd-jwt"' }) + readonly _doctypeAbsentGuard?: unknown; + + @ApiPropertyOptional({ + description: + 'Verifiable Credential Type (required when format is "vc+sd-jwt"; must NOT be provided when format is "mso_mdoc")', + example: 'org.iso.18013.5.1.mDL' + }) + @ValidateIf((o: CreateCredentialTemplateDto) => 'vc+sd-jwt' === o.format) + @IsString() + vct?: string; + + @ValidateIf((o: CreateCredentialTemplateDto) => 'mso_mdoc' === o.format) + @IsEmpty({ message: 'vct must not be provided when format is "mso_mdoc"' }) + readonly _vctAbsentGuard?: unknown; @ApiProperty({ default: false, description: 'Indicates whether credentials can be revoked' }) @IsBoolean() diff --git a/apps/api-gateway/src/issuance/dtos/oidc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts similarity index 94% rename from apps/api-gateway/src/issuance/dtos/oidc-issuer.dto.ts rename to apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts index 89216fcb7..b9e67f423 100644 --- a/apps/api-gateway/src/issuance/dtos/oidc-issuer.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -199,7 +199,7 @@ export class ClientAuthenticationDto { export class AuthorizationServerConfigDto { @ApiProperty({ description: 'Authorization server issuer URL', - example: 'https://example-oidc-provider.com' + example: 'https://example-oid4vc-provider.com' }) @IsString() issuer: string; @@ -248,23 +248,15 @@ export class IssuerCreationDto { description: 'Configuration of the authorization server', type: AuthorizationServerConfigDto }) + @IsOptional() @ValidateNested() @Type(() => AuthorizationServerConfigDto) - authorizationServerConfigs: AuthorizationServerConfigDto; + authorizationServerConfigs?: AuthorizationServerConfigDto; } export class IssuerUpdationDto { issuerId?: string; - @ApiProperty({ - description: 'accessTokenSignerKeyType', - example: 'ed25519' - }) - @IsString({ - message: 'accessTokenSignerKeyType from IssuerCreationDto -> accessTokenSignerKeyType, must be a string' - }) - accessTokenSignerKeyType: string; - @ApiProperty({ description: 'Localized display information for the credential', type: [DisplayDto] diff --git a/apps/api-gateway/src/issuance/oidc.controller.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts similarity index 53% rename from apps/api-gateway/src/issuance/oidc.controller.ts rename to apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts index b37cc4cb4..f88b8718a 100644 --- a/apps/api-gateway/src/issuance/oidc.controller.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts @@ -16,7 +16,9 @@ import { BadRequestException, ParseUUIDPipe, Delete, - Patch + Patch, + Query, + Put } from '@nestjs/common'; import { ApiTags, @@ -33,7 +35,6 @@ import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; import { Response } from 'express'; import IResponseType, { IResponse } from '@credebl/common/interfaces/response.interface'; -import { IssuanceService } from './issuance.service'; import { User } from '../authz/decorators/user.decorator'; import { ResponseMessages } from '@credebl/common/response-messages'; import { Roles } from '../authz/decorators/roles.decorator'; @@ -42,17 +43,23 @@ import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; /* eslint-disable @typescript-eslint/no-unused-vars */ import { user } from '@prisma/client'; -import { IssuerCreationDto, IssuerUpdationDto } from './dtos/oidc-issuer.dto'; -import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oidc-issuer-template.dto'; -import { CreateOidcCredentialOfferDto } from './dtos/issuer-sessions.dto'; -import { IssuanceDto } from './dtos/issuance.dto'; +import { IssuerCreationDto, IssuerUpdationDto } from './dtos/oid4vc-issuer.dto'; +import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oid4vc-issuer-template.dto'; +import { OidcIssueCredentialDto } from './dtos/oid4vc-credential-wh.dto'; +import { Oid4vcIssuanceService } from './oid4vc-issuance.service'; +import { + CreateCredentialOfferD2ADto, + CreateOidcCredentialOfferDto, + GetAllCredentialOfferDto, + UpdateCredentialRequestDto +} from './dtos/issuer-sessions.dto'; @Controller() @UseFilters(CustomExceptionFilter) -@ApiTags('OIDC') +@ApiTags('OID4VC') @ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto }) -export class OidcController { - constructor(private readonly issueCredentialService: IssuanceService) {} +export class Oid4vcIssuanceController { + constructor(private readonly oid4vcIssuanceService: Oid4vcIssuanceService) {} /** * Create issuer against a org(tenant) * @param orgId The ID of the organization @@ -60,9 +67,13 @@ export class OidcController { * @param res The response object * @returns The status of the deletion operation */ - @Post('/orgs/:orgId/oidc/issuers') - @ApiOperation({ summary: 'Create OIDC issuer', description: 'Create OIDC issuer by orgId' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + + @Post('/orgs/:orgId/oid4vc/issuers') + @ApiOperation({ + summary: 'Create OID4VC issuer', + description: 'Creates a new OID4VC issuer for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Issuer created successfully.', type: ApiResponseDto }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -80,7 +91,7 @@ export class OidcController { @Body() issueCredentialDto: IssuerCreationDto, @Res() res: Response ): Promise { - const createIssuer = await this.issueCredentialService.oidcIssuerCreate(issueCredentialDto, orgId, user); + const createIssuer = await this.oid4vcIssuanceService.oidcIssuerCreate(issueCredentialDto, orgId, user); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.oidcIssuer.success.issuerConfig, @@ -89,9 +100,12 @@ export class OidcController { return res.status(HttpStatus.CREATED).json(finalResponse); } - @Post('/orgs/:orgId/oidc/issuers/:issuerId') - @ApiOperation({ summary: 'Update OIDC issuer', description: 'Update OIDC issuer by orgId' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @Post('/orgs/:orgId/oid4vc/issuers/:issuerId') + @ApiOperation({ + summary: 'Update OID4VC issuer', + description: 'Updates an existing OID4VC issuer for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Issuer updated successfully.', type: ApiResponseDto }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -112,7 +126,7 @@ export class OidcController { @Res() res: Response ): Promise { issueCredentialDto.issuerId = issuerId; - const createIssuer = await this.issueCredentialService.oidcIssuerUpdate(issueCredentialDto, orgId, user); + const createIssuer = await this.oid4vcIssuanceService.oidcIssuerUpdate(issueCredentialDto, orgId, user); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.oidcIssuer.success.issuerConfigUpdate, @@ -121,9 +135,9 @@ export class OidcController { return res.status(HttpStatus.CREATED).json(finalResponse); } - @Get('/orgs/:orgId/oidc/issuers/:issuerId') - @ApiOperation({ summary: 'Get OIDC issuer', description: 'Get OIDC issuer by orgId' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @Get('/orgs/:orgId/oid4vc/issuers/:issuerId') + @ApiOperation({ summary: 'Get OID4VC issuer', description: 'Retrieves an OID4VC issuer by issuerId.' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Issuer fetched successfully.', type: ApiResponseDto }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -141,7 +155,7 @@ export class OidcController { issuerId: string, @Res() res: Response ): Promise { - const oidcIssuer = await this.issueCredentialService.oidcGetIssuerById(issuerId, orgId); + const oidcIssuer = await this.oid4vcIssuanceService.oidcGetIssuerById(issuerId, orgId); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.oidcIssuer.success.fetch, @@ -150,9 +164,12 @@ export class OidcController { return res.status(HttpStatus.CREATED).json(finalResponse); } - @Get('/orgs/:orgId/oidc/issuers') - @ApiOperation({ summary: 'Get OIDC issuer', description: 'Get OIDC issuer by orgId' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @Get('/orgs/:orgId/oid4vc/issuers') + @ApiOperation({ + summary: 'Get OID4VC issuers', + description: 'Retrieves all OID4VC issuers for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Issuers fetched successfully.', type: ApiResponseDto }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -168,20 +185,23 @@ export class OidcController { orgId: string, @Res() res: Response ): Promise { - const oidcIssuer = await this.issueCredentialService.oidcGetIssuers(orgId); + const oidcIssuer = await this.oid4vcIssuanceService.oidcGetIssuers(orgId); const finalResponse: IResponse = { - statusCode: HttpStatus.CREATED, + statusCode: HttpStatus.OK, message: ResponseMessages.oidcIssuer.success.fetch, data: oidcIssuer }; - return res.status(HttpStatus.CREATED).json(finalResponse); + return res.status(HttpStatus.OK).json(finalResponse); } - @Delete('/orgs/:orgId/oidc/:issuerId') + @Delete('/orgs/:orgId/oid4vc/:issuerId') + @ApiOperation({ + summary: 'Delete OID4VC issuer', + description: 'Deletes an OID4VC issuer for the specified organization.' + }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - @ApiOperation({ summary: 'Delete oidc issuer' }) async deleteOidcIssuer( @Param( 'orgId', @@ -204,7 +224,7 @@ export class OidcController { @User() user: user, @Res() res: Response ): Promise { - await this.issueCredentialService.oidcDeleteIssuer(user, orgId, issuerId); + await this.oid4vcIssuanceService.oidcDeleteIssuer(user, orgId, issuerId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, @@ -214,9 +234,12 @@ export class OidcController { return res.status(HttpStatus.OK).json(finalResponse); } - @Post('/orgs/:orgId/oidc/:issuerId/template') - @ApiOperation({ summary: 'Create credential template' }) - @ApiResponse({ status: HttpStatus.CREATED, description: 'Template created successfully' }) + @Post('/orgs/:orgId/oid4vc/:issuerId/template') + @ApiOperation({ + summary: 'Create credential template', + description: 'Creates a new credential template for the specified issuer.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Template created successfully.' }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -231,7 +254,7 @@ export class OidcController { ): Promise { CredentialTemplate.issuerId = issuerId; console.log('THis is dto', JSON.stringify(CredentialTemplate, null, 2)); - const template = await this.issueCredentialService.createTemplate(CredentialTemplate, user, orgId, issuerId); + const template = await this.oid4vcIssuanceService.createTemplate(CredentialTemplate, user, orgId, issuerId); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, @@ -242,9 +265,12 @@ export class OidcController { return res.status(HttpStatus.CREATED).json(finalResponse); } - @Get('/orgs/:orgId/oidc/:issuerId/template') - @ApiOperation({ summary: 'List credential templates' }) - @ApiResponse({ status: HttpStatus.OK, description: 'List of templates' }) + @Get('/orgs/:orgId/oid4vc/:issuerId/template') + @ApiOperation({ + summary: 'List credential templates', + description: 'Lists all credential templates for the specified issuer.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'List of templates.' }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -262,15 +288,14 @@ export class OidcController { 'issuerId', new ParseUUIDPipe({ exceptionFactory: (): Error => { - throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + throw new BadRequestException(ResponseMessages.oidcTemplate.error.invalidId); } }) ) issuerId: string, - @User() user: user, @Res() res: Response ): Promise { - const templates = await this.issueCredentialService.findAllTemplate(user, orgId, issuerId); + const templates = await this.oid4vcIssuanceService.findAllTemplate(orgId, issuerId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, @@ -281,8 +306,8 @@ export class OidcController { return res.status(HttpStatus.OK).json(finalResponse); } - @Get('/orgs/:orgId/oidc/:issuerId/template/:templateId') - @ApiOperation({ summary: 'Get credential template by ID' }) + @Get('/orgs/:orgId/oid4vc/:issuerId/template/:templateId') + @ApiOperation({ summary: 'Get credential template by ID', description: 'Retrieves a credential template by its ID.' }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -296,43 +321,29 @@ export class OidcController { }) ) orgId: string, - @Param( - 'issuerId', - new ParseUUIDPipe({ - exceptionFactory: (): Error => { - throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); - } - }) - ) - issuerId: string, - @Param( - 'templateId', - new ParseUUIDPipe({ - exceptionFactory: (): Error => { - throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); - } - }) - ) + @Param('templateId') templateId: string, - @User() user: user, @Res() res: Response ): Promise { - const template = await this.issueCredentialService.findByIdTemplate(user, orgId, templateId, issuerId); + const template = await this.oid4vcIssuanceService.findByIdTemplate(orgId, templateId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, - message: ResponseMessages.oidcTemplate.success.fetch, + message: ResponseMessages.oidcTemplate.success.getById, data: template }; return res.status(HttpStatus.OK).json(finalResponse); } - @Patch('/orgs/:orgId/oidc/:issuerId/template/:templateId') + @Patch('/orgs/:orgId/oid4vc/:issuerId/template/:templateId') + @ApiOperation({ + summary: 'Update credential template', + description: 'Updates a credential template for the specified issuer.' + }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - @ApiOperation({ summary: 'Update credential template' }) async updateTemplate( @Param( 'orgId', @@ -365,7 +376,7 @@ export class OidcController { @Body() dto: UpdateCredentialTemplateDto, @Res() res: Response ): Promise { - const updated = await this.issueCredentialService.updateTemplate(user, orgId, templateId, dto, issuerId); + const updated = await this.oid4vcIssuanceService.updateTemplate(user, orgId, templateId, dto, issuerId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, @@ -376,11 +387,14 @@ export class OidcController { return res.status(HttpStatus.OK).json(finalResponse); } - @Delete('/orgs/:orgId/oidc/:issuerId/template/:templateId') + @Delete('/orgs/:orgId/oid4vc/:issuerId/template/:templateId') + @ApiOperation({ + summary: 'Delete credential template', + description: 'Deletes a credential template for the specified issuer.' + }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - @ApiOperation({ summary: 'Delete credential template' }) async deleteTemplate( @Param( 'orgId', @@ -412,8 +426,7 @@ export class OidcController { @User() user: user, @Res() res: Response ): Promise { - await this.issueCredentialService.deleteTemplate(user, orgId, templateId, issuerId); - + await this.oid4vcIssuanceService.deleteTemplate(user, orgId, templateId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, message: ResponseMessages.oidcTemplate.success.delete @@ -422,15 +435,15 @@ export class OidcController { return res.status(HttpStatus.OK).json(finalResponse); } - @Post('/orgs/:orgId/oidc/:issuerId/create-offer') - @ApiOperation({ summary: 'Create OIDC Credential Offer' }) - @ApiResponse({ - status: HttpStatus.CREATED, - description: `This endpoint creates a new OIDC4VCI credential-offer for a given issuer. It allows clients to request issuance of credentials (e.g., Birth Certificate, Driving License, Student ID) from a registered OIDC issuer using the issuer's ID.` + @Post('/orgs/:orgId/oid4vc/:issuerId/create-offer') + @ApiOperation({ + summary: 'Create OID4VC Credential Offer', + description: 'Creates a new OIDC4VCI credential-offer for a given issuer.' }) - // @ApiBearerAuth() - // @Roles(OrgRoles.OWNER) - // @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Credential offer created successfully.' }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) async createOidcCredentialOffer( @Param('orgId') orgId: string, @@ -441,7 +454,7 @@ export class OidcController { @Res() res: Response ): Promise { oidcCredentialPayload.issuerId = issuerId; - const template = await this.issueCredentialService.createOidcCredentialOffer( + const template = await this.oid4vcIssuanceService.createOidcCredentialOffer( oidcCredentialPayload, user, orgId, @@ -449,32 +462,177 @@ export class OidcController { ); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, - message: ResponseMessages.oidcTemplate.success.create, + message: ResponseMessages.oidcIssuerSession.success.create, data: template }; return res.status(HttpStatus.CREATED).json(finalResponse); } + @Put('/orgs/:orgId/oid4vc/:issuerId/:credentialId/update-offer') + @ApiOperation({ + summary: 'Update OID4VC Credential Offer', + description: 'Updates an existing OIDC4VCI credential-offer.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credential offer updated successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async updateCredentialOffers( + @Body() oidcUpdateCredentialPayload: UpdateCredentialRequestDto, + @Param('orgId') orgId: string, + @Param('issuerId') issuerId: string, + @Param('credentialId') credentialId: string, + @Res() res: Response + ): Promise { + oidcUpdateCredentialPayload.issuerId = issuerId; + oidcUpdateCredentialPayload.credentialOfferId = credentialId; + const updateCredentialOffer = await this.oid4vcIssuanceService.updateOidcCredentialOffer( + oidcUpdateCredentialPayload, + orgId, + issuerId + ); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcIssuerSession.success.update, + data: updateCredentialOffer + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vc/credential-offer/:id') + @ApiOperation({ + summary: 'Get OID4VC credential offer', + description: 'Retrieves an OID4VC credential offer by its ID.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credential offer fetched successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getCredentialOfferDetailsById( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id') + id: string, + @Res() res: Response + ): Promise { + const oidcIssuer = await this.oid4vcIssuanceService.getCredentialOfferDetailsById(id, orgId); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcIssuerSession.success.getById, + data: oidcIssuer + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vc/credential-offer') + @ApiOperation({ + summary: 'Get all OID4VC credential offers', + description: 'Retrieves all OID4VC credential offers for the specified organization.' + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'All credential offers fetched successfully.', + type: ApiResponseDto + }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getAllCredentialOffers( + @Query() getAllCredentialOffer: GetAllCredentialOfferDto, + @Param('orgId') orgId: string, + @Res() res: Response + ): Promise { + const connectionDetails = await this.oid4vcIssuanceService.getAllCredentialOffers(orgId, getAllCredentialOffer); + + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oidcIssuerSession.success.getAll, + data: connectionDetails + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Delete('/orgs/:orgId/oid4vc/:credentialId/delete-offer') + @ApiOperation({ + summary: 'Delete OID4VC credential offer', + description: 'Deletes an OID4VC credential offer for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credential offer deleted successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async deleteCredentialOffers( + @Param('orgId') orgId: string, + @Param('credentialId') credentialId: string, + @Res() res: Response + ): Promise { + const deletedofferDetails = await this.oid4vcIssuanceService.deleteCredentialOffers(orgId, credentialId); + + const finalResponse: IResponse = { + statusCode: HttpStatus.NO_CONTENT, + message: ResponseMessages.oidcIssuerSession.success.delete, + data: deletedofferDetails + }; + return res.status(HttpStatus.NO_CONTENT).json(finalResponse); + } + + @Post('/orgs/:orgId/oid4vc/create-offer/agent') + @ApiOperation({ + summary: 'Create OID4VC Credential Offer direct to agent', + description: 'Creates a new OIDC4VCI credential-offer for a given issuer.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Credential offer created successfully.' }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async createOidcCredentialOfferD2A( + @Param('orgId') + orgId: string, + @Body() oidcCredentialD2APayload: CreateCredentialOfferD2ADto, + @Res() res: Response + ): Promise { + const credentialOffer = await this.oid4vcIssuanceService.createOidcCredentialOfferD2A( + oidcCredentialD2APayload, + orgId + ); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oidcIssuerSession.success.create, + data: credentialOffer + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + } + /** * Catch issue credential webhook responses - * @param oidcIssueCredentialDto The details of the oidc issued credential + * @param oidcIssueCredentialDto The details of the oid4vc issued credential * @param id The ID of the organization * @param res The response object - * @returns The details of the oidc issued credential + * @returns The details of the oid4vc issued credential */ - @Post('wh/:id/credentials') + @Post('wh/:id/openid4vc-issuance') @ApiExcludeEndpoint() @ApiOperation({ - summary: 'Catch OIDC credential states', - description: 'Catch OIDC credential states' + summary: 'Catch OID4VC credential states', + description: 'Handles webhook responses for OID4VC credential issuance.' }) async getIssueCredentialWebhook( - @Body() oidcIssueCredentialDto, + @Body() oidcIssueCredentialDto: OidcIssueCredentialDto, @Param('id') id: string, @Res() res: Response ): Promise { - const getCredentialDetails = await this.issueCredentialService.oidcIssueCredentialWebhook( + console.log('Webhook received:', JSON.stringify(oidcIssueCredentialDto, null, 2)); + const getCredentialDetails = await this.oid4vcIssuanceService.oidcIssueCredentialWebhook( oidcIssueCredentialDto, id ); diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts new file mode 100644 index 000000000..61470bce8 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { Oid4vcIssuanceService } from './oid4vc-issuance.service'; +import { Oid4vcIssuanceController } from './oid4vc-issuance.controller'; +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'; + +@Module({ + imports: [ + HttpModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.ISSUANCE_SERVICE, process.env.API_GATEWAY_NKEY_SEED) + } + ]) + ], + controllers: [Oid4vcIssuanceController], + providers: [Oid4vcIssuanceService, NATSClient] +}) +export class Oid4vcIssuanceModule {} diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts new file mode 100644 index 000000000..b5282d9dc --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts @@ -0,0 +1,138 @@ +import { NATSClient } from '@credebl/common/NATSClient'; +import { Inject, Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { IssuerCreationDto, IssuerUpdationDto } from './dtos/oid4vc-issuer.dto'; +import { BaseService } from 'libs/service/base.service'; +// eslint-disable-next-line camelcase +import { oidc_issuer, user } from '@prisma/client'; +import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oid4vc-issuer-template.dto'; +import { + CreateOidcCredentialOfferDto, + GetAllCredentialOfferDto, + UpdateCredentialRequestDto +} from './dtos/issuer-sessions.dto'; +import { OidcIssueCredentialDto } from './dtos/oid4vc-credential-wh.dto'; + +@Injectable() +export class Oid4vcIssuanceService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly issuanceProxy: ClientProxy, + private readonly natsClient: NATSClient + ) { + super('Oid4vcIssuanceService'); + } + + async oidcIssuerCreate( + issueCredentialDto: IssuerCreationDto, + orgId: string, + userDetails: user + // eslint-disable-next-line camelcase + ): Promise { + const payload = { issueCredentialDto, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-issuer-create', payload); + } + + async oidcIssuerUpdate(issueUpdationDto: IssuerUpdationDto, orgId: string, userDetails: user): Promise { + const payload = { issueUpdationDto, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-issuer-update', payload); + } + + async oidcGetIssuerById(issuerId: string, orgId: string): Promise { + const payload = { issuerId, orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-issuer-get-by-id', payload); + } + + async oidcGetIssuers(orgId: string): Promise { + const payload = { orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-get-issuers-issuance', payload); + } + + async oidcDeleteIssuer(userDetails: user, orgId: string, issuerId: string): Promise { + const payload = { issuerId, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-delete-issuer', payload); + } + + async deleteTemplate(userDetails: user, orgId: string, templateId: string): Promise { + const payload = { templateId, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-template-delete', payload); + } + + async updateTemplate( + userDetails: user, + orgId: string, + templateId: string, + dto: UpdateCredentialTemplateDto, + issuerId: string + ): Promise { + const payload = { templateId, orgId, userDetails, dto, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-template-update', payload); + } + + async findByIdTemplate(orgId: string, templateId: string): Promise { + const payload = { templateId, orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-template-find-id', payload); + } + + async findAllTemplate(orgId: string, issuerId: string): Promise { + const payload = { orgId, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-template-find-all', payload); + } + + async createTemplate( + CredentialTemplate: CreateCredentialTemplateDto, + userDetails: user, + orgId: string, + issuerId: string + ): Promise { + const payload = { CredentialTemplate, orgId, userDetails, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-template-create', payload); + } + + async createOidcCredentialOffer( + oidcCredentialPayload: CreateOidcCredentialOfferDto, + userDetails: user, + orgId: string, + issuerId: string + ): Promise { + const payload = { oidcCredentialPayload, orgId, userDetails, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-create-credential-offer', payload); + } + + async createOidcCredentialOfferD2A(oidcCredentialD2APayload, orgId: string): Promise { + const payload = { oidcCredentialD2APayload, orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-create-credential-offer-D2A', payload); + } + + async updateOidcCredentialOffer( + oidcUpdateCredentialPayload: UpdateCredentialRequestDto, + orgId: string, + issuerId: string + ): Promise { + const payload = { oidcUpdateCredentialPayload, orgId, issuerId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-update-credential-offer', payload); + } + + async getCredentialOfferDetailsById(offerId: string, orgId: string): Promise { + const payload = { offerId, orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-credential-offer-get-by-id', payload); + } + async getAllCredentialOffers(orgId: string, getAllCredentialOffer: GetAllCredentialOfferDto): Promise { + const payload = { orgId, getAllCredentialOffer }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-credential-offer-get-all', payload); + } + + async deleteCredentialOffers(orgId: string, credentialId: string): Promise { + const payload = { orgId, credentialId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-credential-offer-delete', payload); + } + + oidcIssueCredentialWebhook( + oidcIssueCredentialDto: OidcIssueCredentialDto, + id: string + ): Promise<{ + response: object; + }> { + const payload = { oidcIssueCredentialDto, id }; + return this.natsClient.sendNats(this.issuanceProxy, 'webhook-oid4vc-issue-credential', payload); + } +} diff --git a/apps/issuance/constant/issuance.ts b/apps/issuance/constant/issuance.ts index a39a160ca..d32498997 100644 --- a/apps/issuance/constant/issuance.ts +++ b/apps/issuance/constant/issuance.ts @@ -1,4 +1,4 @@ -import { AccessTokenSignerKeyType } from '../interfaces/oidc-issuance.interfaces'; +import { AccessTokenSignerKeyType } from '../interfaces/oid4vc-issuance.interfaces'; export const dpopSigningAlgValuesSupported = ['RS256', 'ES256', 'EdDSA']; export const credentialConfigurationsSupported = {}; diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index 520637516..5caa8b1ba 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -21,11 +21,7 @@ import { Controller, Logger } from '@nestjs/common'; import { IssuanceService } from './issuance.service'; import { MessagePattern } from '@nestjs/microservices'; import { OOBIssueCredentialDto } from 'apps/api-gateway/src/issuance/dtos/issuance.dto'; -import { credential_templates, oidc_issuer, user } from '@prisma/client'; -import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oidc-template.interface'; -import { IssuerCreation, IssuerUpdation } from '../interfaces/oidc-issuance.interfaces'; -import { CreateOidcCredentialOffer } from '../interfaces/oidc-issuer-sessions.interfaces'; -import { OIDCIssuanceService } from './oidc-issuance.service'; +import { user } from '@prisma/client'; @Controller() export class IssuanceController { @@ -144,121 +140,4 @@ export class IssuanceController { async getFileDetailsAndFileDataByFileId(payload: { fileId: string; orgId: string }): Promise { return this.issuanceService.getFileDetailsAndFileDataByFileId(payload.fileId, payload.orgId); } - - @MessagePattern({ cmd: 'oidc-issuer-create' }) - async oidcIssuerCreate(payload: { - issueCredentialDto: IssuerCreation; - orgId: string; - userDetails: user; - }): Promise { - const { issueCredentialDto, orgId, userDetails } = payload; - return this.oidcIssuanceService.oidcIssuerCreate(issueCredentialDto, orgId, userDetails); - } - - @MessagePattern({ cmd: 'oidc-issuer-update' }) - async oidcIssuerUpdate(payload: { - issueUpdationDto: IssuerUpdation; - orgId: string; - userDetails: user; - }): Promise { - const { issueUpdationDto, orgId, userDetails } = payload; - return this.oidcIssuanceService.oidcIssuerUpdate(issueUpdationDto, orgId, userDetails); - } - - @MessagePattern({ cmd: 'oidc-issuer-get-by-id' }) - async oidcGetIssuerById(payload: { - issuerId: string; - orgId: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }): Promise { - const { issuerId, orgId } = payload; - return this.oidcIssuanceService.oidcIssuerGetById(issuerId, orgId); - } - - @MessagePattern({ cmd: 'oidc-get-issuers' }) - async oidcGetIssuers(payload: { orgId: string }): Promise { - const { orgId } = payload; - return this.oidcIssuanceService.oidcIssuers(orgId); - } - - @MessagePattern({ cmd: 'oidc-delete-issuer' }) - async deleteOidcIssuer(payload: { - orgId: string; - userDetails: user; - issuerId: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }): Promise { - const { orgId, userDetails, issuerId } = payload; - return this.oidcIssuanceService.deleteOidcIssuer(orgId, userDetails, issuerId); - } - - @MessagePattern({ cmd: 'oidc-template-create' }) - async oidcTemplateCreate(payload: { - CredentialTemplate: CreateCredentialTemplate; - orgId: string; - issuerId: string; - }): Promise { - const { CredentialTemplate, orgId, issuerId } = payload; - return this.oidcIssuanceService.createTemplate(CredentialTemplate, orgId, issuerId); - } - - @MessagePattern({ cmd: 'oidc-template-update' }) - async oidcTemplateUpdate(payload: { - templateId: string; - dto: UpdateCredentialTemplate; - orgId: string; - issuerId: string; - }): Promise { - const { templateId, dto, orgId, issuerId } = payload; - return this.oidcIssuanceService.updateTemplate(templateId, dto, orgId, issuerId); - } - - @MessagePattern({ cmd: 'oidc-template-delete' }) - async oidcTemplateDelete(payload: { - templateId: string; - orgId: string; - userDetails: user; - issuerId: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }): Promise { - const { templateId, orgId, userDetails, issuerId } = payload; - return this.oidcIssuanceService.deleteTemplate(templateId, orgId, userDetails, issuerId); - } - - @MessagePattern({ cmd: 'oidc-template-find-id' }) - async oidcTemplateFindById(payload: { - templateId: string; - orgId: string; - userDetails: user; - issuerId: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }): Promise { - const { templateId, orgId, userDetails, issuerId } = payload; - return this.oidcIssuanceService.findByIdTemplate(templateId, orgId, userDetails, issuerId); - } - - @MessagePattern({ cmd: 'oidc-template-find-all' }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async oidcTemplateFindAll(payload: { orgId: string; userDetails: user; issuerId: string }): Promise { - const { orgId, userDetails, issuerId } = payload; - return this.oidcIssuanceService.findAllTemplate(orgId, userDetails, issuerId); - } - - @MessagePattern({ cmd: 'oidc-create-credential-offer' }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async createOidcCredentialOffer(payload: { - oidcCredentialPayload: CreateOidcCredentialOffer; - orgId: string; - userDetails: user; - issuerId: string; - }): Promise { - const { oidcCredentialPayload, orgId, userDetails, issuerId } = payload; - return this.oidcIssuanceService.createOidcCredentialOffer(oidcCredentialPayload, orgId, userDetails, issuerId); - } - - //TODO: complete the logic - @MessagePattern({ cmd: 'webhook-oidc-issue-credential' }) - async oidcIssueCredentialWebhook(payload: IssueCredentialWebhookPayload): Promise { - return this.issuanceService.getIssueCredentialWebhook(payload); - } } diff --git a/apps/issuance/src/issuance.module.ts b/apps/issuance/src/issuance.module.ts index b72a0d6a6..76e5cf912 100644 --- a/apps/issuance/src/issuance.module.ts +++ b/apps/issuance/src/issuance.module.ts @@ -20,7 +20,6 @@ import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { ContextInterceptorModule } from '@credebl/context/contextInterceptorModule'; import { GlobalConfigModule } from '@credebl/config/global-config.module'; import { NATSClient } from '@credebl/common/NATSClient'; -import { OIDCIssuanceService } from './oidc-issuance.service'; @Module({ imports: [ @@ -51,7 +50,6 @@ import { OIDCIssuanceService } from './oidc-issuance.service'; controllers: [IssuanceController], providers: [ IssuanceService, - OIDCIssuanceService, IssuanceRepository, UserActivityRepository, PrismaService, diff --git a/apps/issuance/src/issuance.repository.ts b/apps/issuance/src/issuance.repository.ts index 6258a47af..8fa95635a 100644 --- a/apps/issuance/src/issuance.repository.ts +++ b/apps/issuance/src/issuance.repository.ts @@ -14,11 +14,9 @@ import { IssueCredentials, IssuedCredentialStatus } from '../enum/issuance.enum' import { Prisma, agent_invitations, - credential_templates, credentials, file_data, file_upload, - oidc_issuer, org_agents, organisation, platform_config, @@ -32,7 +30,6 @@ import { IIssuedCredentialSearchParams } from 'apps/api-gateway/src/issuance/int import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { PrismaService } from '@credebl/prisma-service'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { IssuerMetadata, IssuerUpdation } from '../interfaces/oidc-issuance.interfaces'; @Injectable() export class IssuanceRepository { @@ -796,186 +793,4 @@ export class IssuanceRepository { throw error; } } - - async getOidcIssuerByOrg(orgId: string): Promise { - try { - return await this.prisma.oidc_issuer.findMany({ - where: { createdBy: orgId }, - include: { - templates: true - }, - orderBy: { - createDateTime: 'desc' - } - }); - } catch (error) { - this.logger.error(`Error in getOidcIssuerByOrg: ${error.message}`); - throw error; - } - } - - async getOidcIssuerDetailsById(issuerId: string): Promise { - try { - return await this.prisma.oidc_issuer.findFirstOrThrow({ - where: { id: issuerId } - }); - } catch (error) { - this.logger.error(`Error in getOidcIssuerDetailsById: ${error.message}`); - throw error; - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async addOidcIssuerDetails(issuerMetadata: IssuerMetadata, issuerProfileJson): Promise { - try { - const { publicIssuerId, createdById, orgAgentId, batchCredentialIssuanceSize } = issuerMetadata; - const oidcIssuerDetails = await this.prisma.oidc_issuer.create({ - data: { - metadata: issuerProfileJson, - publicIssuerId, - createdBy: createdById, - orgAgentId, - batchCredentialIssuanceSize - } - }); - - return oidcIssuerDetails; - } catch (error) { - this.logger.error(`[addOidcIssuerDetails] - error: ${JSON.stringify(error)}`); - throw error; - } - } - - async updateOidcIssuerDetails(createdById: string, issuerConfig: IssuerUpdation): Promise { - try { - const { issuerId, display, batchCredentialIssuanceSize } = issuerConfig; - const oidcIssuerDetails = await this.prisma.oidc_issuer.update({ - where: { id: issuerId }, - data: { - metadata: display as unknown as Prisma.InputJsonValue, - createdBy: createdById, - ...(batchCredentialIssuanceSize !== undefined ? { batchCredentialIssuanceSize } : {}) - } - }); - - return oidcIssuerDetails; - } catch (error) { - this.logger.error(`[addOidcIssuerDetails] - error: ${JSON.stringify(error)}`); - throw error; - } - } - - async deleteOidcIssuer(issuerId: string): Promise { - try { - return await this.prisma.oidc_issuer.delete({ - where: { id: issuerId } - }); - } catch (error) { - this.logger.error(`[deleteOidcIssuer] - error: ${JSON.stringify(error)}`); - throw error; - } - } - - async createTemplate( - issuerId: string, - data: Omit - ): Promise { - try { - return await this.prisma.credential_templates.create({ - data: { - ...data, - issuerId - } - }); - } catch (error) { - this.logger.error(`Error in createTemplate: ${error.message}`); - throw error; - } - } - - async getTemplateById(templateId: string): Promise { - try { - return await this.prisma.credential_templates.findUnique({ - where: { id: templateId } - }); - } catch (error) { - this.logger.error(`Error in getTemplateById: ${error.message}`); - throw error; - } - } - - async getTemplateByIds(templateIds: string[], issuerId: string): Promise { - try { - // Early return if empty input (avoids full table scan if someone passes []) - if (!Array.isArray(templateIds) || 0 === templateIds.length) { - return []; - } - - this.logger.debug(`getTemplateByIds templateIds=${JSON.stringify(templateIds)} issuerId=${issuerId}`); - - return await this.prisma.credential_templates.findMany({ - where: { - id: { in: templateIds }, - issuerId - } - }); - } catch (error) { - this.logger.error(`Error in getTemplateByIds: ${error?.message}`, error?.stack); - throw error; - } - } - - async getTemplateByNameForIssuer(name: string, issuerId: string): Promise { - try { - return await this.prisma.credential_templates.findMany({ - where: { - issuerId, - name: { - equals: name, - mode: 'insensitive' - } - } - }); - } catch (error) { - this.logger.error(`Error in getTemplateByNameForIssuer: ${error.message}`); - throw error; - } - } - - async getTemplatesByIssuerId(issuerId: string): Promise { - try { - return await this.prisma.credential_templates.findMany({ - where: { issuerId }, - orderBy: { - createdAt: 'desc' - } - }); - } catch (error) { - this.logger.error(`Error in getTemplatesByIssuer: ${error.message}`); - throw error; - } - } - - async updateTemplate(templateId: string, data: Partial): Promise { - try { - return await this.prisma.credential_templates.update({ - where: { id: templateId }, - data - }); - } catch (error) { - this.logger.error(`Error in updateTemplate: ${error.message}`); - throw error; - } - } - - async deleteTemplate(templateId: string): Promise { - try { - return await this.prisma.credential_templates.delete({ - where: { id: templateId } - }); - } catch (error) { - this.logger.error(`Error in deleteTemplate: ${error.message}`); - throw error; - } - } } diff --git a/apps/oid4vc-issuance/constant/issuance.ts b/apps/oid4vc-issuance/constant/issuance.ts new file mode 100644 index 000000000..d32498997 --- /dev/null +++ b/apps/oid4vc-issuance/constant/issuance.ts @@ -0,0 +1,6 @@ +import { AccessTokenSignerKeyType } from '../interfaces/oid4vc-issuance.interfaces'; + +export const dpopSigningAlgValuesSupported = ['RS256', 'ES256', 'EdDSA']; +export const credentialConfigurationsSupported = {}; +export const accessTokenSignerKeyType = 'ed25519' as AccessTokenSignerKeyType; +export const batchCredentialIssuanceDefault = 0; diff --git a/apps/issuance/interfaces/oidc-issuance.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts similarity index 89% rename from apps/issuance/interfaces/oidc-issuance.interfaces.ts rename to apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts index 854480f70..1c55048af 100644 --- a/apps/issuance/interfaces/oidc-issuance.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts @@ -1,3 +1,22 @@ +import { organisation } from '@prisma/client'; + +export interface OrgAgent { + organisation: organisation; + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + orgDid: string; + verkey: string; + agentEndPoint: string; + agentId: string; + isDidPublic: boolean; + ledgerId: string; + orgAgentTypeId: string; + tenantId: string; +} + export interface Claim { key: string; label: string; diff --git a/apps/issuance/interfaces/oidc-issuer-sessions.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts similarity index 77% rename from apps/issuance/interfaces/oidc-issuer-sessions.interfaces.ts rename to apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts index e6db80dc0..6fd33ead5 100644 --- a/apps/issuance/interfaces/oidc-issuer-sessions.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts @@ -1,3 +1,5 @@ +import { OpenId4VcIssuanceSessionState } from '@credebl/enum/enum'; + /* --------------------------------------------------------- * Enums * --------------------------------------------------------- */ @@ -36,7 +38,7 @@ export interface CredentialPayload { } export interface CredentialRequest { - credentialSupportedId: string; + credentialSupportedId?: string; templateId: string; format: CredentialFormat; // "vc+sd-jwt" | "mso_mdoc" payload: CredentialPayload; // user-supplied payload (without vct) @@ -49,3 +51,17 @@ export interface CreateOidcCredentialOffer { authenticationType: AuthenticationType; // only option selector credentials: CredentialRequest[]; // one or more credentials } + +export interface GetAllCredentialOffer { + publicIssuerId?: string; + preAuthorizedCode?: string; + state?: OpenId4VcIssuanceSessionState; + credentialOfferUri?: string; + authorizationCode?: string; +} + +export interface UpdateCredentialRequest { + issuerId: string; + credentialOfferId: string; + issuerMetadata: object; +} diff --git a/apps/issuance/interfaces/oidc-template.interface.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts similarity index 86% rename from apps/issuance/interfaces/oidc-template.interface.ts rename to apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts index 2025cfecd..5d2984414 100644 --- a/apps/issuance/interfaces/oidc-template.interface.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -1,5 +1,5 @@ import { Prisma } from '@prisma/client'; -import { Display } from './oidc-issuance.interfaces'; +import { Display } from './oid4vc-issuance.interfaces'; export interface CredentialAttribute { mandatory?: boolean; @@ -21,6 +21,8 @@ export interface CreateCredentialTemplate { attributes: Prisma.JsonValue; appearance?: Prisma.JsonValue; issuerId: string; + vct?: string; + doctype?: string; } export interface UpdateCredentialTemplate extends Partial {} diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts new file mode 100644 index 000000000..3f878528e --- /dev/null +++ b/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts @@ -0,0 +1,48 @@ +export interface CredentialOfferPayload { + credential_issuer: string; + credential_configuration_ids: string[]; + grants: Record; + credentials: Record[]; +} + +export interface IssuanceMetadata { + issuerDid: string; + credentials: Record[]; +} + +export interface OidcIssueCredential { + _tags: Record; + metadata: Record; + issuedCredentials: Record[]; + id: string; + createdAt: string; // ISO date string + issuerId: string; + userPin: string; + preAuthorizedCode: string; + credentialOfferUri: string; + credentialOfferId: string; + credentialOfferPayload: CredentialOfferPayload; + issuanceMetadata: IssuanceMetadata; + state: string; + updatedAt: string; // ISO date string + contextCorrelationId: string; +} + +export interface CredentialOfferWebhookPayload { + credentialOfferId: string; + id: string; + State: string; + contextCorrelationId: string; +} + +export interface CredentialPayload { + orgId: string; + schemaId?: string; + connectionId?: string; + credDefid?: string; + threadId: string; + createdBy: string; + lastChangedBy: string; + state: string; + credentialExchangeId: string; +} diff --git a/apps/issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts similarity index 72% rename from apps/issuance/libs/helpers/credential-sessions.builder.ts rename to apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 6764e0634..85cf77e23 100644 --- a/apps/issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -1,7 +1,7 @@ // builder/credential-offer.builder.ts /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ import { Prisma, credential_templates } from '@prisma/client'; - +import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; /* ============================================================================ Domain Types ============================================================================ */ @@ -65,6 +65,7 @@ export interface ResolvedSignerOption { export interface BuiltCredential { /** e.g., "BirthCertificateCredential-sdjwt" or "DrivingLicenseCredential-mdoc" */ credentialSupportedId: string; + signerOptions?: ResolvedSignerOption; /** Derived from template.format ("vc+sd-jwt" | "mdoc") */ format: CredentialFormat; /** User-provided payload (validated, with vct removed) */ @@ -75,7 +76,7 @@ export interface BuiltCredential { export interface BuiltCredentialOfferBase { /** Resolved signer option (DID or x5c) */ - signerOption: ResolvedSignerOption; + signerOption?: ResolvedSignerOption; /** Normalized credential entries */ credentials: BuiltCredential[]; /** Optional public issuer id to include */ @@ -111,7 +112,7 @@ const isRecord = (v: unknown): v is Record => Boolean(v) && 'ob /** Map DB format string -> API enum */ function mapDbFormatToApiFormat(db: string): CredentialFormat { const norm = db.toLowerCase().replace(/_/g, '-'); - if ('sd-jwt' === norm || 'vc+sd-jwt' === norm || 'sdjwt' === norm) { + if ('sd-jwt' === norm || 'vc+sd-jwt' === norm || 'sdjwt' === norm || 'sd+jwt-vc' === norm) { return CredentialFormat.SdJwtVc; } if ('mdoc' === norm || 'mso-mdoc' === norm || 'mso-mdoc' === norm) { @@ -188,7 +189,8 @@ function ensureTemplateAttributes(v: Prisma.JsonValue): TemplateAttributes { function buildOneCredential( cred: CredentialRequestDtoLike, template: credential_templates, - attrs: TemplateAttributes + attrs: TemplateAttributes, + signerOptions?: SignerOption[] ): BuiltCredential { // 1) Validate payload against template attributes assertMandatoryClaims(cred.payload, attrs, { templateId: cred.templateId }); @@ -206,6 +208,7 @@ function buildOneCredential( return { credentialSupportedId, // e.g., "BirthCertificateCredential-sdjwt" + signerOptions: signerOptions[0], format: apiFormat, // 'vc+sd-jwt' | 'mdoc' payload, // without vct ...(cred.disclosureFrame ? { disclosureFrame: cred.disclosureFrame } : {}) @@ -213,7 +216,7 @@ function buildOneCredential( } /** - * Build the full OIDC credential offer payload. + * Build the full OID4VC credential offer payload. * - Verifies template IDs * - Validates mandatory claims per template * - Normalizes formats & IDs @@ -225,7 +228,7 @@ function buildOneCredential( export function buildCredentialOfferPayload( dto: CreateOidcCredentialOfferDtoLike, templates: credential_templates[], - signerOption: ResolvedSignerOption + signerOptions?: SignerOption[] ): CredentialOfferPayload { // Index templates const byId = new Map(templates.map((t) => [t.id, t])); @@ -240,12 +243,11 @@ export function buildCredentialOfferPayload( const credentials: BuiltCredential[] = dto.credentials.map((cred) => { const template = byId.get(cred.templateId)!; const attrs = ensureTemplateAttributes(template.attributes); // narrow JsonValue safely - return buildOneCredential(cred, template, attrs); + return buildOneCredential(cred, template, attrs, signerOptions); }); // --- Base envelope (issuerId deliberately NOT included) --- const base: BuiltCredentialOfferBase = { - signerOption, // resolved keys (did/x5c) from DB credentials, ...(dto.publicIssuerId ? { publicIssuerId: dto.publicIssuerId } : {}) }; @@ -269,3 +271,79 @@ export function buildCredentialOfferPayload( authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! // definite since !hasPre }; } + +// ----------------------------------------------------------------------------- +// Builder: Update Credential Offer +// ----------------------------------------------------------------------------- +export function buildUpdateCredentialOfferPayload( + dto: CreateOidcCredentialOfferDtoLike, + templates: credential_templates[] +): { credentials: BuiltCredential[] } { + // Index templates by id + const byId = new Map(templates.map((t) => [t.id, t])); + + // Validate all templateIds exist + const unknown = dto.credentials.map((c) => c.templateId).filter((id) => !byId.has(id)); + if (unknown.length) { + throw new Error(`Unknown template ids: ${unknown.join(', ')}`); + } + + // Validate each credential against its template + const credentials: BuiltCredential[] = dto.credentials.map((cred) => { + const template = byId.get(cred.templateId)!; + const attrs = ensureTemplateAttributes(template.attributes); // safely narrow JsonValue + + // check that all payload keys exist in template attributes + const payloadKeys = Object.keys(cred.payload); + const invalidKeys = payloadKeys.filter((k) => !attrs[k]); + if (invalidKeys.length) { + throw new Error(`Invalid attributes for template "${cred.templateId}": ${invalidKeys.join(', ')}`); + } + + // also validate mandatory fields are present + assertMandatoryClaims(cred.payload, attrs, { templateId: cred.templateId }); + + // build minimal normalized credential (no vct, issuerId, etc.) + const apiFormat = mapDbFormatToApiFormat(template.format); + const suffix = formatSuffix(apiFormat); + const credentialSupportedId = `${template.name}-${suffix}`; + return { + credentialSupportedId, + format: apiFormat, + payload: cred.payload, + ...(cred.disclosureFrame ? { disclosureFrame: cred.disclosureFrame } : {}) + }; + }); + + // Only return credentials array here (update flow doesn't need preAuth/auth configs) + return { + credentials + }; +} + +export function buildCredentialOfferUrl(baseUrl: string, getAllCredentialOffer: GetAllCredentialOffer): string { + const criteriaParams: string[] = []; + + if (getAllCredentialOffer.publicIssuerId) { + criteriaParams.push(`publicIssuerId=${encodeURIComponent(getAllCredentialOffer.publicIssuerId)}`); + } + + if (getAllCredentialOffer.preAuthorizedCode) { + criteriaParams.push(`preAuthorizedCode=${encodeURIComponent(getAllCredentialOffer.preAuthorizedCode)}`); + } + + if (getAllCredentialOffer.state) { + criteriaParams.push(`state=${encodeURIComponent(getAllCredentialOffer.state)}`); + } + + if (getAllCredentialOffer.credentialOfferUri) { + criteriaParams.push(`credentialOfferUri=${encodeURIComponent(getAllCredentialOffer.credentialOfferUri)}`); + } + + if (getAllCredentialOffer.authorizationCode) { + criteriaParams.push(`authorizationCode=${encodeURIComponent(getAllCredentialOffer.authorizationCode)}`); + } + + // Append query string if any params exist + return 0 < criteriaParams.length ? `${baseUrl}?${criteriaParams.join('&')}` : baseUrl; +} diff --git a/apps/issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts similarity index 84% rename from apps/issuance/libs/helpers/issuer.metadata.ts rename to apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index f9950f807..22735c10a 100644 --- a/apps/issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -1,7 +1,8 @@ /* eslint-disable camelcase */ import { oidc_issuer, Prisma } from '@prisma/client'; import { batchCredentialIssuanceDefault } from '../../constant/issuance'; -import { CreateOidcCredentialOffer } from 'apps/issuance/interfaces/oidc-issuer-sessions.interfaces'; +import { CreateOidcCredentialOffer } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; +import { IssuerResponse } from 'apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces'; type AttributeDisplay = { name: string; locale: string }; type AttributeDef = { @@ -46,7 +47,6 @@ type CredentialConfigurationsSupported = { // ---- Static Lists (as requested) ---- const STATIC_CREDENTIAL_ALGS = ['ES256', 'EdDSA'] as const; const STATIC_BINDING_METHODS = ['did:key'] as const; -const DOCTYPE = 'org.iso.18013.5.1'; // for mso_mdoc format const MSO_MDOC = 'mso_mdoc'; // alternative format value // Safe coercion helpers @@ -95,6 +95,7 @@ export function buildCredentialConfigurationsSupported( templates: TemplateRowPrisma[], opts?: { vct?: string; + doctype?: string; scopeVct?: string; keyResolver?: (t: TemplateRowPrisma) => string; format?: string; @@ -117,14 +118,23 @@ export function buildCredentialConfigurationsSupported( // ---- dynamic format per row ---- // eslint-disable-next-line @typescript-eslint/no-explicit-any const rowFormat = (t as any).format ?? format; + const isMdoc = rowFormat === `${MSO_MDOC}`; const suffix = rowFormat === `${MSO_MDOC}` ? 'mdoc' : 'sdjwt'; // key: keep your keyResolver override; otherwise include suffix const key = 'function' === typeof opts?.keyResolver ? opts.keyResolver(t) : `${t.name}-${suffix}`; // vct/scope: vct only for non-mdoc; scope always uses suffix - const vct = opts?.vct ?? t.name; - const scopeBase = opts?.scopeVct ?? vct; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rowDoctype: string | undefined = opts?.doctype ?? (t as any).doctype; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rowVct: string = opts?.vct ?? (t as any).vct ?? t.name; + + if (isMdoc && !rowDoctype) { + throw new Error(`Template ${t.id}: doctype is required for mdoc format`); + } + + const scopeBase = opts?.scopeVct ?? rowVct; const scope = `openid4vc:credential:${scopeBase}-${suffix}`; const claims = Object.fromEntries( Object.entries(attrs).map(([claimName, def]) => { @@ -155,9 +165,7 @@ export function buildCredentialConfigurationsSupported( credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], display, - ...(rowFormat === `${MSO_MDOC}` - ? { doctype: `${DOCTYPE}` } // static for mdoc - : { vct }) // keep vct only for non-mdoc + ...(isMdoc ? { doctype: rowDoctype as string } : { vct: rowVct }) }; } @@ -206,7 +214,7 @@ function isDisplayArray(x: unknown): x is DisplayItem[] { * Build issuer metadata payload from issuer row + credential configurations. * * @param credentialConfigurations Object with credentialConfigurationsSupported (from your template builder) - * @param oidcIssuer OIDC issuer row (uses publicIssuerId and metadata -> display) + * @param oidcIssuer OID4VC issuer row (uses publicIssuerId and metadata -> display) * @param opts Optional overrides: dpopAlgs[], accessTokenSignerKeyType */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -242,3 +250,20 @@ export function extractTemplateIds(offer: CreateOidcCredentialOffer): string[] { return offer.credentials.map((c) => c.templateId).filter((id): id is string => Boolean(id)); } + +export function normalizeJson(input: unknown): IssuerResponse { + if ('string' === typeof input) { + return JSON.parse(input) as IssuerResponse; + } + if (input && 'object' === typeof input) { + return input as IssuerResponse; + } + throw new Error('Expected a JSON object or JSON string'); +} + +export function encodeIssuerPublicId(publicIssuerId: string): string { + if (!publicIssuerId) { + throw new Error('issuerPublicId is required'); + } + return encodeURIComponent(publicIssuerId.trim()); +} diff --git a/apps/oid4vc-issuance/src/main.ts b/apps/oid4vc-issuance/src/main.ts new file mode 100644 index 000000000..3c6933fc9 --- /dev/null +++ b/apps/oid4vc-issuance/src/main.ts @@ -0,0 +1,23 @@ +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 { Oid4vcIssuanceModule } from './oid4vc-issuance.module'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice(Oid4vcIssuanceModule, { + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.ISSUANCE_SERVICE, process.env.ISSUANCE_NKEY_SEED) + }); + app.useLogger(app.get(NestjsLoggerServiceAdapter)); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('OID4VC-Issuance-Service Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts new file mode 100644 index 000000000..7d9f476f7 --- /dev/null +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts @@ -0,0 +1,182 @@ +/* eslint-disable camelcase */ +import { Controller } from '@nestjs/common'; +import { Oid4vcIssuanceService } from './oid4vc-issuance.service'; +import { IssuerCreation, IssuerUpdation } from '../interfaces/oid4vc-issuance.interfaces'; +import { MessagePattern } from '@nestjs/microservices'; +import { user, oidc_issuer, credential_templates } from '@prisma/client'; +import { + CreateOidcCredentialOffer, + UpdateCredentialRequest, + GetAllCredentialOffer +} from '../interfaces/oid4vc-issuer-sessions.interfaces'; +import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oid4vc-template.interfaces'; +import { CredentialOfferWebhookPayload } from '../interfaces/oid4vc-wh-interfaces'; + +@Controller() +export class Oid4vcIssuanceController { + constructor(private readonly oid4vcIssuanceService: Oid4vcIssuanceService) {} + + @MessagePattern({ cmd: 'oid4vc-issuer-create' }) + async oidcIssuerCreate(payload: { + issueCredentialDto: IssuerCreation; + orgId: string; + userDetails: user; + }): Promise { + const { issueCredentialDto, orgId, userDetails } = payload; + return this.oid4vcIssuanceService.oidcIssuerCreate(issueCredentialDto, orgId, userDetails); + } + + @MessagePattern({ cmd: 'oid4vc-issuer-update' }) + async oidcIssuerUpdate(payload: { + issueUpdationDto: IssuerUpdation; + orgId: string; + userDetails: user; + }): Promise { + const { issueUpdationDto, orgId, userDetails } = payload; + return this.oid4vcIssuanceService.oidcIssuerUpdate(issueUpdationDto, orgId, userDetails); + } + + @MessagePattern({ cmd: 'oid4vc-issuer-get-by-id' }) + async oidcGetIssuerById(payload: { + issuerId: string; + orgId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { issuerId, orgId } = payload; + return this.oid4vcIssuanceService.oidcIssuerGetById(issuerId, orgId); + } + + @MessagePattern({ cmd: 'oid4vc-get-issuers-issuance' }) + async oidcGetIssuers(payload: { orgId: string }): Promise { + const { orgId } = payload; + return this.oid4vcIssuanceService.oidcIssuers(orgId); + } + + @MessagePattern({ cmd: 'oid4vc-delete-issuer' }) + async deleteOidcIssuer(payload: { + orgId: string; + userDetails: user; + issuerId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { orgId, userDetails, issuerId } = payload; + return this.oid4vcIssuanceService.deleteOidcIssuer(orgId, userDetails, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-template-create' }) + async oidcTemplateCreate(payload: { + CredentialTemplate: CreateCredentialTemplate; + orgId: string; + issuerId: string; + }): Promise { + const { CredentialTemplate, orgId, issuerId } = payload; + return this.oid4vcIssuanceService.createTemplate(CredentialTemplate, orgId, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-template-update' }) + async oidcTemplateUpdate(payload: { + templateId: string; + dto: UpdateCredentialTemplate; + orgId: string; + issuerId: string; + }): Promise { + const { templateId, dto, orgId, issuerId } = payload; + return this.oid4vcIssuanceService.updateTemplate(templateId, dto, orgId, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-template-delete' }) + async oidcTemplateDelete(payload: { + templateId: string; + orgId: string; + userDetails: user; + issuerId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { templateId, orgId, userDetails, issuerId } = payload; + return this.oid4vcIssuanceService.deleteTemplate(templateId, orgId, userDetails, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-template-find-id' }) + async oidcTemplateFindById(payload: { templateId: string; orgId: string }): Promise { + const { templateId, orgId } = payload; + return this.oid4vcIssuanceService.findByIdTemplate(templateId, orgId); + } + + @MessagePattern({ cmd: 'oid4vc-template-find-all' }) + async oidcTemplateFindAll(payload: { orgId: string; issuerId: string }): Promise { + const { orgId, issuerId } = payload; + return this.oid4vcIssuanceService.findAllTemplate(orgId, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-create-credential-offer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async createOidcCredentialOffer(payload: { + oidcCredentialPayload: CreateOidcCredentialOffer; + orgId: string; + userDetails: user; + issuerId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { oidcCredentialPayload, orgId, userDetails, issuerId } = payload; + return this.oid4vcIssuanceService.createOidcCredentialOffer(oidcCredentialPayload, orgId, userDetails, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-create-credential-offer-D2A' }) + async createOidcCredentialOfferD2A(payload: { + oidcCredentialD2APayload; + orgId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { oidcCredentialD2APayload, orgId } = payload; + return this.oid4vcIssuanceService.createOidcCredentialOfferD2A(oidcCredentialD2APayload, orgId); + } + + @MessagePattern({ cmd: 'oid4vc-update-credential-offer' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async updateOidcCredentialOffer(payload: { + oidcUpdateCredentialPayload: UpdateCredentialRequest; + orgId: string; + issuerId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { oidcUpdateCredentialPayload, orgId, issuerId } = payload; + return this.oid4vcIssuanceService.updateOidcCredentialOffer(oidcUpdateCredentialPayload, orgId, issuerId); + } + + @MessagePattern({ cmd: 'oid4vc-credential-offer-get-by-id' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async getCredentialOfferDetailsById(payload: { + offerId: string; + orgId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { offerId, orgId } = payload; + return this.oid4vcIssuanceService.getCredentialOfferDetailsById(offerId, orgId); + } + @MessagePattern({ cmd: 'oid4vc-credential-offer-get-all' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async getAllCredentialOffers(payload: { + orgId: string; + getAllCredentialOffer: GetAllCredentialOffer; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { orgId, getAllCredentialOffer } = payload; + return this.oid4vcIssuanceService.getCredentialOffers(orgId, getAllCredentialOffer); + } + + @MessagePattern({ cmd: 'oid4vc-credential-offer-delete' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async deleteCredentialOffers(payload: { + orgId: string; + credentialId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }): Promise { + const { orgId, credentialId } = payload; + return this.oid4vcIssuanceService.deleteCredentialOffers(orgId, credentialId); + } + + @MessagePattern({ cmd: 'webhook-oid4vc-issue-credential' }) + async oidcIssueCredentialWebhook(payload: CredentialOfferWebhookPayload): Promise { + return this.oid4vcIssuanceService.storeOidcCredentialWebhook(payload); + } +} diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.module.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.module.ts new file mode 100644 index 000000000..b882032cb --- /dev/null +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.module.ts @@ -0,0 +1,36 @@ +import { Logger, Module } from '@nestjs/common'; +import { Oid4vcIssuanceController } from './oid4vc-issuance.controller'; +import { Oid4vcIssuanceService } from './oid4vc-issuance.service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonModule } from '@credebl/common'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { GlobalConfigModule } from '@credebl/config'; +import { ContextInterceptorModule } from '@credebl/context'; +import { LoggerModule } from '@credebl/logger'; +import { CacheModule } from '@nestjs/cache-manager'; +import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; +import { Oid4vcIssuanceRepository } from './oid4vc-issuance.repository'; +import { NATSClient } from '@credebl/common/NATSClient'; +import { PrismaService } from '@credebl/prisma-service'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.OIDC4VC_ISSUANCE_SERVICE, process.env.OIDC4VC_ISSUANCE_NKEY_SEED) + } + ]), + CommonModule, + GlobalConfigModule, + LoggerModule, + PlatformConfig, + ContextInterceptorModule, + CacheModule.register() + ], + controllers: [Oid4vcIssuanceController], + providers: [Oid4vcIssuanceService, Oid4vcIssuanceRepository, PrismaService, Logger, NATSClient] +}) +export class Oid4vcIssuanceModule {} diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts new file mode 100644 index 000000000..6c12475fb --- /dev/null +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts @@ -0,0 +1,278 @@ +/* eslint-disable camelcase */ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +// eslint-disable-next-line camelcase +import { Prisma, credential_templates, oidc_issuer, org_agents } from '@prisma/client'; +import { PrismaService } from '@credebl/prisma-service'; +import { IssuerMetadata, IssuerUpdation, OrgAgent } from '../interfaces/oid4vc-issuance.interfaces'; +import { ResponseMessages } from '@credebl/common/response-messages'; + +@Injectable() +export class Oid4vcIssuanceRepository { + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} + + async getAgentEndPoint(orgId: string): Promise { + try { + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { + orgId + }, + include: { + organisation: true + } + }); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + return agentDetails; + } catch (error) { + this.logger.error(`Error in get getAgentEndPoint: ${error.message} `); + throw error; + } + } + + async getOrgAgentType(orgAgentId: string): Promise { + try { + const { agent } = await this.prisma.org_agents_type.findFirst({ + where: { + id: orgAgentId + } + }); + + return agent; + } catch (error) { + this.logger.error(`[getOrgAgentType] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async getOrganizationByTenantId(tenantId: string): Promise { + try { + return this.prisma.org_agents.findFirst({ + where: { + tenantId + } + }); + } catch (error) { + this.logger.error(`Error in getOrganization in issuance repository: ${error.message} `); + throw error; + } + } + + async storeOidcCredentialDetails(credentialPayload): Promise { + try { + const { credentialOfferId, state, offerId, contextCorrelationId, orgId } = credentialPayload; + const credentialDetails = await this.prisma.oid4vc_credentials.upsert({ + where: { + offerId + }, + update: { + lastChangedBy: orgId, + credentialOfferId, + contextCorrelationId, + state + }, + create: { + lastChangedBy: orgId, + createdBy: orgId, + state, + orgId, + credentialOfferId, + offerId, + contextCorrelationId + } + }); + + return credentialDetails; + } catch (error) { + this.logger.error(`Error in get storeOidcCredentialDetails: ${error.message} `); + throw error; + } + } + + async getOidcIssuerByOrg(orgId: string): Promise { + try { + return await this.prisma.oidc_issuer.findMany({ + where: { createdBy: orgId }, + include: { + templates: true + }, + orderBy: { + createDateTime: 'desc' + } + }); + } catch (error) { + this.logger.error(`Error in getOidcIssuerByOrg: ${error.message}`); + throw error; + } + } + + async getOidcIssuerDetailsById(issuerId: string): Promise { + try { + return await this.prisma.oidc_issuer.findFirstOrThrow({ + where: { id: issuerId } + }); + } catch (error) { + this.logger.error(`Error in getOidcIssuerDetailsById: ${error.message}`); + throw error; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async addOidcIssuerDetails(issuerMetadata: IssuerMetadata, issuerProfileJson): Promise { + try { + const { publicIssuerId, createdById, orgAgentId, batchCredentialIssuanceSize } = issuerMetadata; + const oidcIssuerDetails = await this.prisma.oidc_issuer.create({ + data: { + metadata: issuerProfileJson, + publicIssuerId, + createdBy: createdById, + orgAgentId, + batchCredentialIssuanceSize + } + }); + + return oidcIssuerDetails; + } catch (error) { + this.logger.error(`[addOidcIssuerDetails] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async updateOidcIssuerDetails(createdById: string, issuerConfig: IssuerUpdation): Promise { + try { + const { issuerId, display, batchCredentialIssuanceSize } = issuerConfig; + const oidcIssuerDetails = await this.prisma.oidc_issuer.update({ + where: { id: issuerId }, + data: { + metadata: display as unknown as Prisma.InputJsonValue, + createdBy: createdById, + ...(batchCredentialIssuanceSize !== undefined ? { batchCredentialIssuanceSize } : {}) + } + }); + + return oidcIssuerDetails; + } catch (error) { + this.logger.error(`[addOidcIssuerDetails] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async deleteOidcIssuer(issuerId: string): Promise { + try { + return await this.prisma.oidc_issuer.delete({ + where: { id: issuerId } + }); + } catch (error) { + this.logger.error(`[deleteOidcIssuer] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async createTemplate( + issuerId: string, + data: Omit + ): Promise { + try { + return await this.prisma.credential_templates.create({ + data: { + ...data, + issuerId + } + }); + } catch (error) { + this.logger.error(`Error in createTemplate: ${error.message}`); + throw error; + } + } + + async getTemplateById(templateId: string): Promise { + try { + return await this.prisma.credential_templates.findUnique({ + where: { id: templateId } + }); + } catch (error) { + this.logger.error(`Error in getTemplateById: ${error.message}`); + throw error; + } + } + + async getTemplateByIds(templateIds: string[], issuerId: string): Promise { + try { + // Early return if empty input (avoids full table scan if someone passes []) + if (!Array.isArray(templateIds) || 0 === templateIds.length) { + return []; + } + + this.logger.debug(`getTemplateByIds templateIds=${JSON.stringify(templateIds)} issuerId=${issuerId}`); + + return await this.prisma.credential_templates.findMany({ + where: { + id: { in: templateIds }, + issuerId + } + }); + } catch (error) { + this.logger.error(`Error in getTemplateByIds: ${error?.message}`, error?.stack); + throw error; + } + } + + async getTemplateByNameForIssuer(name: string, issuerId: string): Promise { + try { + return await this.prisma.credential_templates.findMany({ + where: { + issuerId, + name: { + equals: name, + mode: 'insensitive' + } + } + }); + } catch (error) { + this.logger.error(`Error in getTemplateByNameForIssuer: ${error.message}`); + throw error; + } + } + + async getTemplatesByIssuerId(issuerId: string): Promise { + try { + return await this.prisma.credential_templates.findMany({ + where: { issuerId }, + orderBy: { + createdAt: 'desc' + } + }); + } catch (error) { + this.logger.error(`Error in getTemplatesByIssuer: ${error.message}`); + throw error; + } + } + + async updateTemplate(templateId: string, data: Partial): Promise { + try { + return await this.prisma.credential_templates.update({ + where: { id: templateId }, + data + }); + } catch (error) { + this.logger.error(`Error in updateTemplate: ${error.message}`); + throw error; + } + } + + async deleteTemplate(templateId: string): Promise { + try { + return await this.prisma.credential_templates.delete({ + where: { id: templateId } + }); + } catch (error) { + this.logger.error(`Error in deleteTemplate: ${error.message}`); + throw error; + } + } +} diff --git a/apps/issuance/src/oidc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts similarity index 51% rename from apps/issuance/src/oidc-issuance.service.ts rename to apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index da743deb6..afdf46be6 100644 --- a/apps/issuance/src/oidc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -17,7 +17,7 @@ import { NotFoundException, Scope } from '@nestjs/common'; -import { IssuanceRepository } from './issuance.repository'; +import { Oid4vcIssuanceRepository } from './oid4vc-issuance.repository'; import { CommonConstants } from '@credebl/common/common.constant'; import { ResponseMessages } from '@credebl/common/response-messages'; import { ClientProxy, RpcException } from '@nestjs/microservices'; @@ -31,8 +31,8 @@ import { IssuerMetadata, IssuerResponse, IssuerUpdation -} from '../interfaces/oidc-issuance.interfaces'; -import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oidc-template.interface'; +} from '../interfaces/oid4vc-issuance.interfaces'; +import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oid4vc-template.interfaces'; import { accessTokenSignerKeyType, batchCredentialIssuanceDefault, @@ -42,15 +42,21 @@ import { import { buildCredentialConfigurationsSupported, buildIssuerPayload, - extractTemplateIds + encodeIssuerPublicId, + extractTemplateIds, + normalizeJson } from '../libs/helpers/issuer.metadata'; import { CreateOidcCredentialOffer, + GetAllCredentialOffer, SignerMethodOption, - SignerOption -} from '../interfaces/oidc-issuer-sessions.interfaces'; -import { BadRequestErrorDto } from 'apps/api-gateway/src/dtos/bad-request-error.dto'; -import { buildCredentialOfferPayload, CredentialOfferPayload } from '../libs/helpers/credential-sessions.builder'; + UpdateCredentialRequest +} from '../interfaces/oid4vc-issuer-sessions.interfaces'; +import { + buildCredentialOfferPayload, + buildCredentialOfferUrl, + CredentialOfferPayload +} from '../libs/helpers/credential-sessions.builder'; type CredentialDisplayItem = { logo?: { uri: string; alt_text?: string }; @@ -63,22 +69,22 @@ type Appearance = { display: CredentialDisplayItem[]; }; @Injectable() -export class OIDCIssuanceService { +export class Oid4vcIssuanceService { private readonly logger = new Logger('IssueCredentialService'); constructor( @Inject('NATS_CLIENT') private readonly issuanceServiceProxy: ClientProxy, - private readonly issuanceRepository: IssuanceRepository + private readonly oid4vcIssuanceRepository: Oid4vcIssuanceRepository ) {} async oidcIssuerCreate(issuerCreation: IssuerCreation, orgId: string, userDetails: user): Promise { try { const { issuerId, batchCredentialIssuanceSize } = issuerCreation; - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint, id: orgAgentId, orgAgentTypeId } = agentDetails; - const orgAgentType = await this.issuanceRepository.getOrgAgentType(orgAgentTypeId); + const orgAgentType = await this.oid4vcIssuanceRepository.getOrgAgentType(orgAgentTypeId); if (!orgAgentType) { throw new NotFoundException(ResponseMessages.issuance.error.orgAgentTypeNotFound); } @@ -86,7 +92,7 @@ export class OIDCIssuanceService { const issuerInitialConfig: IssuerInitialConfig = { issuerId, display: issuerCreation?.display || {}, - authorizationServerConfigs: issuerCreation?.authorizationServerConfigs || {}, + authorizationServerConfigs: issuerCreation?.authorizationServerConfigs || undefined, accessTokenSignerKeyType, dpopSigningAlgValuesSupported, batchCredentialIssuance: { @@ -119,13 +125,13 @@ export class OIDCIssuanceService { orgAgentId, batchCredentialIssuanceSize: issuerCreation?.batchCredentialIssuanceSize }; - const addOidcIssuerDetails = await this.issuanceRepository.addOidcIssuerDetails( + const addOidcIssuerDetails = await this.oid4vcIssuanceRepository.addOidcIssuerDetails( issuerMetadata, issuerCreation?.display ); if (!addOidcIssuerDetails) { - throw new InternalServerErrorException('Error in adding OIDC Issuer details in DB'); + throw new InternalServerErrorException('Error in adding OID4VC Issuer details in DB'); } return addOidcIssuerDetails; } catch (error) { @@ -136,27 +142,29 @@ export class OIDCIssuanceService { async oidcIssuerUpdate(issuerUpdationConfig: IssuerUpdation, orgId: string, userDetails: user): Promise { try { - const getIssuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerUpdationConfig.issuerId); + const getIssuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById( + issuerUpdationConfig.issuerId + ); if (!getIssuerDetails) { throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); } - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } - const { agentEndPoint, id: orgAgentId, orgAgentTypeId } = agentDetails; - const orgAgentType = await this.issuanceRepository.getOrgAgentType(orgAgentTypeId); + const { agentEndPoint, orgAgentTypeId } = agentDetails; + const orgAgentType = await this.oid4vcIssuanceRepository.getOrgAgentType(orgAgentTypeId); if (!orgAgentType) { throw new NotFoundException(ResponseMessages.issuance.error.orgAgentTypeNotFound); } - const addOidcIssuerDetails = await this.issuanceRepository.updateOidcIssuerDetails( + const addOidcIssuerDetails = await this.oid4vcIssuanceRepository.updateOidcIssuerDetails( userDetails.id, issuerUpdationConfig ); if (!addOidcIssuerDetails) { - throw new InternalServerErrorException('Error in updating OIDC Issuer details in DB'); + throw new InternalServerErrorException('Error in updating OID4VC Issuer details in DB'); } const url = await getAgentUrl( @@ -165,6 +173,7 @@ export class OIDCIssuanceService { getIssuerDetails.publicIssuerId ); const issuerConfig = await this.buildOidcIssuerConfig(issuerUpdationConfig.issuerId); + console.log('This is the issuerConfig:', JSON.stringify(issuerConfig, null, 2)); const updatedIssuer = await this._createOIDCTemplate(issuerConfig, url, orgId); if (updatedIssuer?.response?.statusCode && 200 !== updatedIssuer?.response?.statusCode) { throw new InternalServerErrorException( @@ -181,86 +190,96 @@ export class OIDCIssuanceService { async oidcIssuerGetById(issuerId: string, orgId: string): Promise { try { - const getIssuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + const getIssuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + console.log('This is the getIssuerDetails:', JSON.stringify(getIssuerDetails, null, 2)); if (!getIssuerDetails && getIssuerDetails.publicIssuerId) { throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); } - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } - const url = await getAgentUrl( - agentDetails?.agentEndPoint, - CommonConstants.OIDC_ISSUER_BY_ID, - getIssuerDetails?.publicIssuerId - ); + console.log('This is the agentDetails:', getIssuerDetails?.publicIssuerId); + const encodedId = encodeIssuerPublicId(getIssuerDetails?.publicIssuerId); + const url = await getAgentUrl(agentDetails?.agentEndPoint, CommonConstants.OIDC_ISSUER_BY_ID, encodedId); + console.log('This is the oidcIssuerGetById url:', url); const issuerDetailsRaw = await this._oidcGetIssuerById(url, orgId); - + console.log('This is the issuerDetailsRaw:', JSON.stringify(issuerDetailsRaw, null, 2)); if (!issuerDetailsRaw) { throw new InternalServerErrorException(`Error from agent while getting issuer`); } - - // response is a string → parse it into IssuerResponse const issuerDetails = { - response: JSON.parse(issuerDetailsRaw.response) as IssuerResponse + response: normalizeJson(issuerDetailsRaw.response) }; return issuerDetails.response; } catch (error) { - this.logger.error(`[oidcIssuerGetById] - error in oidcIssuerGetById issuance records: ${JSON.stringify(error)}`); - throw new RpcException(error?.response ?? error); + const errorStack = error?.status?.message?.error; + if (errorStack) { + throw new RpcException({ + message: errorStack?.reason ? errorStack?.reason : errorStack?.message, + statusCode: error?.status?.code + }); + } else { + throw new RpcException(error.response ? error.response : error); + } } } - async oidcIssuers(orgId: string): Promise { + async oidcIssuers(orgId: string): Promise { try { - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); - if (!agentDetails) { + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails?.agentEndPoint) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } - const url = await getAgentUrl(agentDetails?.agentEndPoint, CommonConstants.OIDC_GET_ALL_ISSUERS); + + const url = await getAgentUrl(agentDetails.agentEndPoint, CommonConstants.OIDC_GET_ALL_ISSUERS); const issuersDetails = await this._oidcGetIssuers(url, orgId); - if (!issuersDetails) { - throw new InternalServerErrorException(`Error from agent while oidcIssuers`); + if (!issuersDetails || null == issuersDetails.response) { + throw new InternalServerErrorException('Error from agent while oidcIssuers'); } + //TODO: Fix the response type from agent + const raw = issuersDetails.response as unknown; + const response: IssuerResponse[] = + 'string' === typeof raw ? (JSON.parse(raw) as IssuerResponse[]) : (raw as IssuerResponse[]); - return issuersDetails; - } catch (error) { - this.logger.error(`[oidcIssuers] - error in oidcIssuers issuance records: ${JSON.stringify(error)}`); + if (!Array.isArray(response)) { + throw new InternalServerErrorException('Invalid issuer payload from agent'); + } + return response; + } catch (error: any) { + const msg = error?.message ?? 'unknown error'; + this.logger.error(`[oidcIssuers] - error in oidcIssuers: ${msg}`); throw new RpcException(error?.response ?? error); } } async deleteOidcIssuer(orgId: string, userDetails: user, issuerId: string) { try { - const deleteOidcIssuer = await this.issuanceRepository.deleteOidcIssuer(issuerId); + const deleteOidcIssuer = await this.oid4vcIssuanceRepository.deleteOidcIssuer(issuerId); if (!deleteOidcIssuer) { - throw new NotFoundException(ResponseMessages.oidcTemplate.error.deleteTemplate); + throw new NotFoundException(ResponseMessages.oidcIssuer.error.deleteFailed); } - const issuerRecordId = await this.oidcIssuerGetById(issuerId, orgId); if (!issuerRecordId.id) { throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); } - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint } = agentDetails; - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); - if (!issuerDetails) { - throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); - } const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_DELETE, issuerRecordId.id); - const createTemplateOnAgent = await this._deleteOidcIssuer(url, orgId); if (!createTemplateOnAgent) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } return deleteOidcIssuer; } catch (error) { - this.logger.error(`[deleteTemplate] - error: ${JSON.stringify(error)}`); - throw new RpcException(error.response ?? error); + if ('PrismaClientKnownRequestError' === error.name) { + throw new BadRequestException(error.meta?.cause ?? ResponseMessages.oidcIssuer.error.deleteFailed); + } + throw new Error(error.response ? error.response : error); } } @@ -270,8 +289,9 @@ export class OIDCIssuanceService { issuerId: string ): Promise { try { - const { name, description, format, canBeRevoked, attributes, appearance, signerOption } = CredentialTemplate; - const checkNameExist = await this.issuanceRepository.getTemplateByNameForIssuer(name, issuerId); + const { name, description, format, canBeRevoked, attributes, appearance, signerOption, vct, doctype } = + CredentialTemplate; + const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer(name, issuerId); if (0 < checkNameExist.length) { throw new ConflictException(ResponseMessages.oidcTemplate.error.templateNameAlreadyExist); } @@ -286,17 +306,24 @@ export class OIDCIssuanceService { signerOption }; // Persist in DB - const createdTemplate = await this.issuanceRepository.createTemplate(issuerId, metadata); + const createdTemplate = await this.oid4vcIssuanceRepository.createTemplate(issuerId, metadata); if (!createdTemplate) { throw new InternalServerErrorException(ResponseMessages.oidcTemplate.error.createFailed); } - const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + let opts = {}; + if (vct) { + opts = { ...opts, vct }; + } + if (doctype) { + opts = { ...opts, doctype }; + } + const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId, opts); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint } = agentDetails; - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); if (!issuerDetails) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); } @@ -319,12 +346,12 @@ export class OIDCIssuanceService { issuerId: string ): Promise { try { - const template = await this.issuanceRepository.getTemplateById(templateId); + const template = await this.oid4vcIssuanceRepository.getTemplateById(templateId); if (!template) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.notFound); } if (updateCredentialTemplate.name) { - const checkNameExist = await this.issuanceRepository.getTemplateByNameForIssuer( + const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer( updateCredentialTemplate.name, issuerId ); @@ -348,19 +375,19 @@ export class OIDCIssuanceService { ...(issuerId ? { issuerId } : {}) }; - const updatedTemplate = await this.issuanceRepository.updateTemplate(templateId, payload); + const updatedTemplate = await this.oid4vcIssuanceRepository.updateTemplate(templateId, payload); - const templates = await this.issuanceRepository.getTemplatesByIssuerId(issuerId); + const templates = await this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); if (!templates || 0 === templates.length) { throw new NotFoundException(ResponseMessages.issuance.error.notFound); } const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint } = agentDetails; - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); if (!issuerDetails) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); } @@ -381,22 +408,22 @@ export class OIDCIssuanceService { // eslint-disable-next-line @typescript-eslint/no-explicit-any async deleteTemplate(templateId: string, orgId: string, userDetails: user, issuerId: string): Promise { try { - const template = await this.issuanceRepository.getTemplateById(templateId); + const template = await this.oid4vcIssuanceRepository.getTemplateById(templateId); if (!template) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.notFound); } - const deleteTemplate = await this.issuanceRepository.deleteTemplate(templateId); + const deleteTemplate = await this.oid4vcIssuanceRepository.deleteTemplate(templateId); if (!deleteTemplate) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.deleteTemplate); } const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint } = agentDetails; - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); if (!issuerDetails) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); } @@ -413,15 +440,16 @@ export class OIDCIssuanceService { } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async findByIdTemplate(templateId: string, orgId: string, userDetails: user, issuerId: string): Promise { + async findByIdTemplate(templateId: string, orgId: string): Promise { try { - const template = await this.issuanceRepository.getTemplateById(templateId); + if (!orgId || !templateId) { + throw new BadRequestException(ResponseMessages.oidcTemplate.error.invalidId); + } + const template = await this.oid4vcIssuanceRepository.getTemplateById(templateId); if (!template) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.notFound); } - - return { message: ResponseMessages.oidcTemplate.success.fetch, data: template }; + return template; } catch (error) { this.logger.error(`[findByIdTemplate] - error: ${JSON.stringify(error)}`); throw new RpcException(error.response ?? error); @@ -439,26 +467,36 @@ export class OIDCIssuanceService { if (!filterTemplateIds) { throw new BadRequestException('Please provide a valid id'); } - const getAllOfferTemplates = await this.issuanceRepository.getTemplateByIds(filterTemplateIds, issuerId); + const getAllOfferTemplates = await this.oid4vcIssuanceRepository.getTemplateByIds(filterTemplateIds, issuerId); if (!getAllOfferTemplates) { throw new NotFoundException('No templates found for the issuer'); } - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); //TDOD: signerOption should be under credentials change this with x509 support - const signerOption: SignerOption = { - method: SignerMethodOption.DID, - did: agentDetails.orgDid - }; + + //TDOD: signerOption should be under credentials change this with x509 support + const signerOptions = []; + getAllOfferTemplates.forEach((template) => { + if (template.signerOption === SignerMethodOption.DID) { + signerOptions.push({ + method: SignerMethodOption.DID, + did: agentDetails.orgDid + }); + } + }); + //TODO: Implement x509 support and discuss with team const buildOidcCredentialOffer: CredentialOfferPayload = buildCredentialOfferPayload( createOidcCredentialOffer, getAllOfferTemplates, - signerOption + signerOptions ); + console.log('This is the buildOidcCredentialOffer:', JSON.stringify(buildOidcCredentialOffer, null, 2)); + if (!buildOidcCredentialOffer) { - throw new BadRequestException('Error while creating oidc credential offer'); + throw new BadRequestException('Error while creating oid4vc credential offer'); } - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); if (!issuerDetails) { throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); } @@ -471,7 +509,7 @@ export class OIDCIssuanceService { const createCredentialOfferOnAgent = await this._oidcCreateCredentialOffer(buildOidcCredentialOffer, url, orgId); if (!createCredentialOfferOnAgent) { - throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errroCreateOffer); + throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errorCreateOffer); } return createCredentialOfferOnAgent.response; @@ -480,13 +518,153 @@ export class OIDCIssuanceService { throw new RpcException(error.response ?? error); } } + + async createOidcCredentialOfferD2A(oidcCredentialD2APayload, orgId: string): Promise { + try { + for (const credential of oidcCredentialD2APayload.credentials) { + const { signerOptions } = credential; + + if (!signerOptions?.method) { + throw new BadRequestException(`signerOptions.method is required`); + } + if (signerOptions.method === SignerMethodOption.X5C) { + if (!signerOptions.x5c || 0 === signerOptions.x5c.length) { + // const x5cFromDb = await this.oid4vcIssuanceRepository.getIssuerX5c( + const x5cFromDb = 'Test'; + // If you want to use the actual DB call, uncomment and use: + // const x5cFromDb = await this.oid4vcIssuanceRepository.getIssuerX5c( + // oidcCredentialD2APayload.publicIssuerId, + // orgId + // ); + if (!x5cFromDb || 0 === x5cFromDb.length) { + throw new BadRequestException(`No x5c found for issuer`); + } + signerOptions.x5c = x5cFromDb; + } + } + + if (signerOptions.method === SignerMethodOption.DID) { + if (!signerOptions.did) { + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new BadRequestException(`No DID found for issuer`); + } + signerOptions.did = agentDetails.orgDid; + } + } + } + + const url = await getAgentUrl( + await this.getAgentEndpoint(orgId), + CommonConstants.OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER + ); + const createCredentialOfferOnAgent = await this._oidcCreateCredentialOffer(oidcCredentialD2APayload, url, orgId); + if (!createCredentialOfferOnAgent) { + throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errorCreateOffer); + } + console.log('This is the createCredentialOfferOnAgent:', JSON.stringify(createCredentialOfferOnAgent, null, 2)); + + return createCredentialOfferOnAgent.response; + } catch (error) { + this.logger.error(`[createOidcCredentialOffer] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async updateOidcCredentialOffer( + updateOidcCredentialOffer: UpdateCredentialRequest, + orgId: string, + issuerId: string + ): Promise { + try { + if (!updateOidcCredentialOffer.issuerMetadata) { + throw new BadRequestException('Please provide a valid issuerMetadata'); + } + const url = await getAgentUrl( + await this.getAgentEndpoint(orgId), + CommonConstants.OIDC_ISSUER_SESSIONS_UPDATE_OFFER, + updateOidcCredentialOffer.credentialOfferId + ); + const updateCredentialOfferOnAgent = await this._oidcUpdateCredentialOffer( + updateOidcCredentialOffer.issuerMetadata, + url, + orgId + ); + console.log('This is the updateCredentialOfferOnAgent:', JSON.stringify(updateCredentialOfferOnAgent)); + if (!updateCredentialOfferOnAgent) { + throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errorUpdateOffer); + } + + return updateCredentialOfferOnAgent.response; + } catch (error) { + this.logger.error(`[createOidcCredentialOffer] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async getCredentialOfferDetailsById(offerId: string, orgId: string): Promise { + try { + const url = await getAgentUrl( + await this.getAgentEndpoint(orgId), + CommonConstants.OIDC_ISSUER_SESSIONS_BY_ID, + offerId + ); + const offer = await this._oidcGetCredentialOfferById(url, orgId); + if ('string' === typeof offer.response) { + offer.response = JSON.parse(offer.response); + } + return offer.response; + } catch (error) { + this.logger.error(`[getCredentialOfferDetailsById] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + + async getCredentialOffers(orgId: string, getAllCredentialOffer: GetAllCredentialOffer): Promise { + try { + const url = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.OIDC_ISSUER_SESSIONS); + const credentialOfferUrl = buildCredentialOfferUrl(url, getAllCredentialOffer); + const offers = await this._oidcGetCredentialOfferById(credentialOfferUrl, orgId); + if ('string' === typeof offers.response) { + offers.response = JSON.parse(offers.response); + } + return offers.response; + } catch (error) { + this.logger.error(`[getCredentialOffers] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + async deleteCredentialOffers(orgId: string, credentialId: string): Promise { + try { + if (!credentialId) { + throw new BadRequestException('Please provide a valid credentialId'); + } + const url = await getAgentUrl( + await this.getAgentEndpoint(orgId), + CommonConstants.OIDC_DELETE_CREDENTIAL_OFFER, + credentialId + ); + const deletedCredentialOffer = await this._oidcDeleteCredentialOffer(url, orgId); + if (!deletedCredentialOffer) { + throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.deleteFailed); + } + if ('string' === typeof deletedCredentialOffer.response) { + deletedCredentialOffer.response = JSON.parse(deletedCredentialOffer.response); + } + return deletedCredentialOffer.response; + } catch (error) { + this.logger.error(`[getCredentialOffers] - error: ${JSON.stringify(error)}`); + throw new RpcException(error.response ?? error); + } + } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - async buildOidcIssuerConfig(issuerId: string) { + async buildOidcIssuerConfig(issuerId: string, configMetadata?) { try { - const issuerDetails = await this.issuanceRepository.getOidcIssuerDetailsById(issuerId); - const templates = await this.issuanceRepository.getTemplatesByIssuerId(issuerId); + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + const templates = await this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); - const credentialConfigurationsSupported = buildCredentialConfigurationsSupported(templates); + const credentialConfigurationsSupported = buildCredentialConfigurationsSupported(templates, configMetadata); return buildIssuerPayload(credentialConfigurationsSupported, issuerDetails); } catch (error) { @@ -495,11 +673,12 @@ export class OIDCIssuanceService { } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async findAllTemplate(orgId: string, userDetails: user, issuerId: string): Promise { + async findAllTemplate(orgId: string, issuerId: string): Promise { try { - const templates = await this.issuanceRepository.getTemplatesByIssuerId(issuerId); - return { message: ResponseMessages.oidcTemplate.success.fetch, data: templates }; + if (!orgId || !issuerId) { + throw new BadRequestException(ResponseMessages.oidcTemplate.error.invalidId); + } + return this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); } catch (error) { this.logger.error(`[findAllTemplate] - error: ${JSON.stringify(error)}`); throw new RpcException(error.response ?? error); @@ -509,67 +688,125 @@ export class OIDCIssuanceService { // eslint-disable-next-line @typescript-eslint/no-explicit-any async _createOIDCIssuer(issuerCreation, url: string, orgId: string): Promise { try { - const pattern = { cmd: 'agent-create-oidc-issuer' }; + const pattern = { cmd: 'agent-create-oid4vc-issuer' }; const payload: IAgentOIDCIssuerCreate = { issuerCreation, url, orgId }; return this.natsCall(pattern, payload); } catch (error) { - this.logger.error(`[_createOIDCIssuer] [NATS call]- error in create OIDC Issuer : ${JSON.stringify(error)}`); + this.logger.error(`[_createOIDCIssuer] [NATS call]- error in create OID4VC Issuer : ${JSON.stringify(error)}`); throw error; } } async _createOIDCTemplate(templatePayload, url: string, orgId: string): Promise { try { - const pattern = { cmd: 'agent-create-oidc-template' }; + const pattern = { cmd: 'agent-create-oid4vc-template' }; const payload = { templatePayload, url, orgId }; return this.natsCall(pattern, payload); } catch (error) { - this.logger.error(`[_createOIDCTemplate] [NATS call]- error in create OIDC Template : ${JSON.stringify(error)}`); + this.logger.error( + `[_createOIDCTemplate] [NATS call]- error in create OID4VC Template : ${JSON.stringify(error)}` + ); throw error; } } async _deleteOidcIssuer(url: string, orgId: string): Promise { try { - const pattern = { cmd: 'delete-oidc-issuer' }; + const pattern = { cmd: 'delete-oid4vc-issuer' }; const payload = { url, orgId }; return this.natsCall(pattern, payload); } catch (error) { - this.logger.error(`[_createOIDCTemplate] [NATS call]- error in create OIDC Template : ${JSON.stringify(error)}`); + this.logger.error( + `[_createOIDCTemplate] [NATS call]- error in create OID4VC Template : ${JSON.stringify(error)}` + ); throw error; } } async _oidcGetIssuerById(url: string, orgId: string) { try { - const pattern = { cmd: 'oidc-get-issuer-by-id' }; + const pattern = { cmd: 'oid4vc-get-issuer-by-id' }; const payload = { url, orgId }; return this.natsCall(pattern, payload); } catch (error) { - this.logger.error(`[_oidcGetIssuerById] [NATS call]- error in oidc get issuer by id : ${JSON.stringify(error)}`); + this.logger.error( + `[_oidcGetIssuerById] [NATS call]- error in oid4vc get issuer by id : ${JSON.stringify(error)}` + ); throw error; } } async _oidcGetIssuers(url: string, orgId: string) { try { - const pattern = { cmd: 'oidc-get-issuers' }; + const pattern = { cmd: 'oid4vc-get-issuers-agent-service' }; const payload = { url, orgId }; return this.natsCall(pattern, payload); } catch (error) { - this.logger.error(`[_oidcGetIssuers] [NATS call]- error in oidc get issuers : ${JSON.stringify(error)}`); + this.logger.error(`[_oidcGetIssuers] [NATS call]- error in oid4vc get issuers : ${JSON.stringify(error)}`); throw error; } } async _oidcCreateCredentialOffer(credentialPayload: CredentialOfferPayload, url: string, orgId: string) { try { - const pattern = { cmd: 'agent-service-oidc-create-credential-offer' }; + const pattern = { cmd: 'agent-service-oid4vc-create-credential-offer' }; const payload = { credentialPayload, url, orgId }; return this.natsCall(pattern, payload); } catch (error) { this.logger.error( - `[_oidcCreateCredentialOffer] [NATS call]- error in oidc create credential offer : ${JSON.stringify(error)}` + `[_oidcCreateCredentialOffer] [NATS call]- error in oid4vc create credential offer : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _oidcUpdateCredentialOffer(issuanceMetadata, url: string, orgId: string) { + try { + const pattern = { cmd: 'agent-service-oid4vc-update-credential-offer' }; + const payload = { issuanceMetadata, url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_oidcUpdateCredentialOffer] [NATS call]- error in oid4vc update credential offer : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _oidcGetCredentialOfferById(url: string, orgId: string) { + try { + const pattern = { cmd: 'agent-service-oid4vc-get-credential-offer-by-id' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_oidcGetCredentialOfferById] [NATS call]- error in oid4vc get credential offer by id : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _oidcGetCredentialOffers(url: string, orgId: string) { + try { + const pattern = { cmd: 'agent-service-oid4vc-get-credential-offers' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_oidcGetCredentialOffers] [NATS call]- error in oid4vc get credential offers : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _oidcDeleteCredentialOffer(url: string, orgId: string) { + try { + const pattern = { cmd: 'agent-service-oid4vc-delete-credential-offer' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_oidcDeleteCredentialOffer] [NATS call]- error in oid4vc delete credential offer : ${JSON.stringify(error)}` ); throw error; } @@ -607,7 +844,7 @@ export class OIDCIssuanceService { } async getAgentEndpoint(orgId: string): Promise { - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); @@ -619,4 +856,34 @@ export class OIDCIssuanceService { return agentDetails.agentEndPoint; } + + async storeOidcCredentialWebhook(CredentialOfferWebhookPayload): Promise { + try { + console.log('Storing OID4VC Credential Webhook:', CredentialOfferWebhookPayload); + const { credentialOfferId, state, id, contextCorrelationId } = CredentialOfferWebhookPayload; + let orgId: string; + if ('default' !== contextCorrelationId) { + const getOrganizationId = await this.oid4vcIssuanceRepository.getOrganizationByTenantId(contextCorrelationId); + orgId = getOrganizationId?.orgId; + } else { + orgId = id; + } + + const credentialPayload = { + orgId, + offerId: id, + credentialOfferId, + state, + contextCorrelationId + }; + + const agentDetails = await this.oid4vcIssuanceRepository.storeOidcCredentialDetails(credentialPayload); + return agentDetails; + } catch (error) { + this.logger.error( + `[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}` + ); + throw new RpcException(error.response ? error.response : error); + } + } } diff --git a/apps/oid4vc-issuance/test/app.e2e-spec.ts b/apps/oid4vc-issuance/test/app.e2e-spec.ts new file mode 100644 index 000000000..37bf09beb --- /dev/null +++ b/apps/oid4vc-issuance/test/app.e2e-spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { Oid4vcIssuanceModule } from './../src/oid4vc-issuance.module'; + +describe('Oid4vcIssuanceController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [Oid4vcIssuanceModule] + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => request(app.getHttpServer()).get('/').expect(200).expect('Hello World!')); +}); diff --git a/apps/oid4vc-issuance/test/jest-e2e.json b/apps/oid4vc-issuance/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/oid4vc-issuance/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/oid4vc-issuance/tsconfig.app.json b/apps/oid4vc-issuance/tsconfig.app.json new file mode 100644 index 000000000..b5f8c422e --- /dev/null +++ b/apps/oid4vc-issuance/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/oid4vc-issuance" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index be75ccede..ccd3a1751 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -117,12 +117,14 @@ export enum CommonConstants { // CREATE KEYS CREATE_POLYGON_SECP256k1_KEY = '/polygon/create-keys', - // OIDC URLs + // OID4VC URLs URL_OIDC_ISSUER_CREATE = '/openid4vc/issuer', /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase, @typescript-eslint/no-duplicate-enum-values */ URL_OIDC_GET_ISSUES = '/openid4vc/issuer', URL_OIDC_ISSUER_UPDATE = '/openid4vc/issuer/#', URL_OIDC_ISSUER_SESSIONS_CREATE = '/openid4vc/issuance-sessions/create-credential-offer', + URL_OIDC_ISSUER_SESSIONS_GET = '/openid4vc/issuance-sessions/#', + URL_OIDC_ISSUER_SESSIONS_GET_ALL = '/openid4vc/issuance-sessions', // Nested attribute separator NESTED_ATTRIBUTE_SEPARATOR = '~', @@ -371,6 +373,7 @@ export enum CommonConstants { NOTIFICATION_SERVICE = 'notification', GEO_LOCATION_SERVICE = 'geo-location', CLOUD_WALLET_SERVICE = 'cloud-wallet', + OIDC4VC_ISSUANCE_SERVICE = 'oid4vc-issuance', ACCEPT_OFFER = '/didcomm/credentials/accept-offer', SEED_LENGTH = 32, @@ -392,13 +395,17 @@ export enum CommonConstants { SEND_QUESTION = 'send-question', SEND_BASIC_MESSAGE = 'send-basic-message', - // OIDC - OIDC_ISSUER_CREATE = 'create-oidc-issuer', - OIDC_ISSUER_DELETE = 'delete-oidc-issuer', - OIDC_GET_ALL_ISSUERS = 'get-all-oidc-issuers', + // OID4VC + OIDC_ISSUER_CREATE = 'create-oid4vc-issuer', + OIDC_ISSUER_DELETE = 'delete-oid4vc-issuer', + OIDC_GET_ALL_ISSUERS = 'get-all-oid4vc-issuers', OIDC_ISSUER_BY_ID = 'get-issuer-by-id', - OIDC_ISSUER_TEMPLATE = 'create-oidc-template', - OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER = 'create-oidc-credential-offer' + OIDC_ISSUER_TEMPLATE = 'create-oid4vc-template', + OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER = 'create-oid4vc-credential-offer', + OIDC_ISSUER_SESSIONS_UPDATE_OFFER = 'update-oid4vc-credential-offer', + OIDC_ISSUER_SESSIONS_BY_ID = 'get-oid4vc-session-by-id', + OIDC_ISSUER_SESSIONS = 'get-oid4vc-sessions', + OIDC_DELETE_CREDENTIAL_OFFER = 'delete-oid4vc-credential-offer' } export const MICRO_SERVICE_NAME = Symbol('MICRO_SERVICE_NAME'); export const ATTRIBUTE_NAME_REGEX = /\['(.*?)'\]/; diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index b1c70e012..5091239fc 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -102,7 +102,11 @@ export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramI [ String(CommonConstants.OIDC_ISSUER_SESSIONS_CREDENTIAL_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_CREATE) - ] + ], + [String(CommonConstants.OIDC_ISSUER_SESSIONS_UPDATE_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET)], + [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)] ]); const urlSuffix = agentUrlMap.get(urlFlag); diff --git a/libs/common/src/nats.config.ts b/libs/common/src/nats.config.ts index 7fd1ff011..984459979 100644 --- a/libs/common/src/nats.config.ts +++ b/libs/common/src/nats.config.ts @@ -14,8 +14,8 @@ export const getNatsOptions = ( const baseOptions = { servers: `${process.env.NATS_URL}`.split(','), maxReconnectAttempts: NATSReconnects.maxReconnectAttempts, - reconnectTimeWait: NATSReconnects.reconnectTimeWait, - queue: serviceName + reconnectTimeWait: NATSReconnects.reconnectTimeWait + // queue: serviceName }; if (nkeySeed) { diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 11d53a256..f76c899eb 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -505,34 +505,35 @@ export const ResponseMessages = { }, oidcIssuer: { success: { - create: 'OIDC issuer created successfully.', - update: 'OIDC issuer updated successfully.', - delete: 'OIDC issuer deleted successfully.', - fetch: 'OIDC issuer(s) fetched successfully.', + create: 'OID4VC issuer created successfully.', + update: 'OID4VC issuer updated successfully.', + delete: 'OID4VC issuer deleted successfully.', + fetch: 'OID4VC issuer(s) fetched successfully.', issuerConfig: 'Issuer config details created successfully', issuerConfigUpdate: 'Issuer config details updated successfully' }, error: { - notFound: 'OIDC issuer not found.', - invalidId: 'Invalid OIDC issuer ID.', - createFailed: 'Failed to create OIDC issuer.', - updateFailed: 'Failed to update OIDC issuer.', - deleteFailed: 'Failed to delete OIDC issuer.' + notFound: 'OID4VC issuer not found.', + invalidId: 'Invalid OID4VC issuer ID.', + createFailed: 'Failed to create OID4VC issuer.', + updateFailed: 'Failed to update OID4VC issuer.', + deleteFailed: 'Failed to delete OID4VC issuer.' } }, oidcTemplate: { success: { - create: 'OIDC template created successfully.', - update: 'OIDC template updated successfully.', - delete: 'OIDC template deleted successfully.', - fetch: 'OIDC template(s) fetched successfully.' + create: 'OID4VC template created successfully.', + update: 'OID4VC template updated successfully.', + delete: 'OID4VC template deleted successfully.', + fetch: 'OID4VC template(s) fetched successfully.', + getById: 'OID4VC template details fetched successfully.' }, error: { - notFound: 'OIDC template not found.', - invalidId: 'Invalid OIDC template ID.', - createFailed: 'Failed to create OIDC template.', - updateFailed: 'Failed to update OIDC template.', - deleteFailed: 'Failed to delete OIDC template.', + notFound: 'OID4VC template not found.', + invalidId: 'Invalid OID4VC template ID.', + createFailed: 'Failed to create OID4VC template.', + updateFailed: 'Failed to update OID4VC template.', + deleteFailed: 'Failed to delete OID4VC template.', issuerDisplayNotFound: 'Issuer display not found.', issuerDetailsNotFound: 'Issuer details not found.', templateNameAlreadyExist: 'Template name already exists for this issuer.', @@ -541,14 +542,16 @@ export const ResponseMessages = { }, oidcIssuerSession: { success: { - create: 'OIDC Credential offer created successfully.' + create: 'OID4VC Credential offer created successfully.', + getById: 'OID4VC Credential offer details fetched successfully.', + getAll: 'OID4VC Credential offers fetched successfully.', + update: 'OID4VC Credential offer updated successfully.', + delete: 'OID4VC Credential offer deleted successfully.' }, error: { - errroCreateOffer: 'Error while creating OIDC credential offer on agent.', - invalidId: 'Invalid OIDC issuer ID.', - createFailed: 'Failed to create OIDC issuer.', - updateFailed: 'Failed to update OIDC issuer.', - deleteFailed: 'Failed to delete OIDC issuer.' + errorCreateOffer: 'Error while creating OID4VC credential offer on agent.', + errorUpdateOffer: 'Error while updating OID4VC credential offer on agent.', + deleteFailed: 'Failed to delete OID4VC credential offer.' } }, nats: { diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 9dc8b31ca..787fc1f92 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -271,3 +271,16 @@ export enum ProviderType { KEYCLOAK = 'keycloak', SUPABASE = 'supabase' } + +export declare enum OpenId4VcIssuanceSessionState { + OfferCreated = 'OfferCreated', + OfferUriRetrieved = 'OfferUriRetrieved', + AuthorizationInitiated = 'AuthorizationInitiated', + AuthorizationGranted = 'AuthorizationGranted', + AccessTokenRequested = 'AccessTokenRequested', + AccessTokenCreated = 'AccessTokenCreated', + CredentialRequestReceived = 'CredentialRequestReceived', + CredentialsPartiallyIssued = 'CredentialsPartiallyIssued', + Completed = 'Completed', + Error = 'Error' +} diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 05fada62d..54f3d7840 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -151,6 +151,7 @@ model organisation { agent_invitations agent_invitations[] credential_definition credential_definition[] file_upload file_upload[] + oid4vc_credentials oid4vc_credentials[] } model org_invitations { @@ -583,6 +584,21 @@ model oidc_issuer { @@index([orgAgentId]) } +model oid4vc_credentials { + id String @id @default(uuid()) @db.Uuid + orgId String @db.Uuid + offerId String @unique + credentialOfferId String + state String + contextCorrelationId String + createdBy String @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + + organisation organisation @relation(fields: [orgId], references: [id]) +} + enum SignerOption { did x509 @@ -604,4 +620,5 @@ model credential_templates { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt signerOption SignerOption -} \ No newline at end of file +} + diff --git a/nest-cli.json b/nest-cli.json index ee0cc7122..819afa8b7 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -286,6 +286,15 @@ "compilerOptions": { "tsConfigPath": "libs/logger/tsconfig.lib.json" } + }, + "oid4vc-issuance": { + "type": "application", + "root": "apps/oid4vc-issuance", + "entryFile": "main", + "sourceRoot": "apps/oid4vc-issuance/src", + "compilerOptions": { + "tsConfigPath": "apps/oid4vc-issuance/tsconfig.app.json" + } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 418a81be2..6a1e02570 100644 --- a/package.json +++ b/package.json @@ -209,4 +209,4 @@ "engines": { "node": ">=18" } -} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 74a5fa904..d0da5851f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -104,7 +104,8 @@ "@credebl/context/*": [ "libs/context/src/*" ] - } + }, + "baseUrl": "./" }, "exclude": [ "node_modules", From a3ee18c3f2bf9e9655382f59323343d38df25f41 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Tue, 7 Oct 2025 12:29:44 +0530 Subject: [PATCH 03/43] refactor: updated the docker file for oid4vc-issuance Signed-off-by: Tipu_Singh --- Dockerfiles/Dockerfile.oid4vc-issuance | 10 +++---- .../oid4vc-issuance.controller.ts | 4 +-- pnpm-lock.yaml | 26 +++++++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Dockerfiles/Dockerfile.oid4vc-issuance b/Dockerfiles/Dockerfile.oid4vc-issuance index d127d2e9a..ee861dc93 100644 --- a/Dockerfiles/Dockerfile.oid4vc-issuance +++ b/Dockerfiles/Dockerfile.oid4vc-issuance @@ -8,6 +8,7 @@ WORKDIR /app # Copy package.json and package-lock.json COPY package.json ./ +COPY pnpm-workspace.yaml ./ #COPY package-lock.json ./ ENV PUPPETEER_SKIP_DOWNLOAD=true @@ -20,8 +21,9 @@ COPY . . # RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate RUN cd libs/prisma-service && npx prisma generate -# Build the issuance service -RUN pnpm run build issuance +# Build the oid4vc-issuance service +RUN npm run build oid4vc-issuance + # Stage 2: Create the final image FROM node:18-alpine @@ -38,8 +40,6 @@ COPY --from=build /app/dist/apps/oid4vc-issuance/ ./dist/apps/oid4vc-issuance/ COPY --from=build /app/libs/ ./libs/ #COPY --from=build /app/package.json ./ COPY --from=build /app/node_modules ./node_modules -# COPY --from=build /app/uploadedFiles ./uploadedFiles - # Set the command to run the microservice -CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/oid4vc-issuance/main.js"] +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/oid4vc-issuance/main.js"] \ No newline at end of file diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts index f88b8718a..f6d29066e 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts @@ -56,8 +56,8 @@ import { @Controller() @UseFilters(CustomExceptionFilter) @ApiTags('OID4VC') -@ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto }) -@ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto }) +@ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) export class Oid4vcIssuanceController { constructor(private readonly oid4vcIssuanceService: Oid4vcIssuanceService) {} /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8be044baf..96bc3e38d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ catalogs: '@nestjs/cli': specifier: ^11.0.10 version: 11.0.10 + '@nestjs/cli': + specifier: ^11.0.10 + version: 11.0.10 '@nestjs/common': specifier: ^11.1.6 version: 11.1.6 @@ -48,6 +51,9 @@ catalogs: '@nestjs/schematics': specifier: ^11.0.7 version: 11.0.7 + '@nestjs/schematics': + specifier: ^11.0.7 + version: 11.0.7 '@nestjs/swagger': specifier: ^11.2.0 version: 11.2.0 @@ -72,6 +78,18 @@ catalogs: nestjs-typeorm-paginate: specifier: ^4.0.4 version: 4.1.0 + nestjs-cls: + specifier: ^6.0.1 + version: 6.0.1 + nestjs-rate-limiter: + specifier: ^3.1.0 + version: 3.1.0 + nestjs-supabase-auth: + specifier: ^1.0.9 + version: 1.0.9 + nestjs-typeorm-paginate: + specifier: ^4.0.4 + version: 4.1.0 importers: @@ -276,15 +294,20 @@ importers: specifier: ^2.15.1 version: 2.29.3 nestjs-cls: + specifier: 'catalog:' specifier: 'catalog:' version: 6.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) nestjs-rate-limiter: + specifier: 'catalog:' specifier: 'catalog:' version: 3.1.0 nestjs-supabase-auth: + specifier: 'catalog:' specifier: 'catalog:' version: 1.0.9 nestjs-typeorm-paginate: + specifier: 'catalog:' + version: 4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))) specifier: 'catalog:' version: 4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))) node-html-to-image: @@ -379,12 +402,15 @@ importers: version: 1.6.11 devDependencies: '@nestjs/cli': + specifier: 'catalog:' specifier: 'catalog:' version: 11.0.10(@types/node@20.19.17) '@nestjs/schematics': + specifier: 'catalog:' specifier: 'catalog:' version: 11.0.7(chokidar@4.0.3)(typescript@5.9.2) '@nestjs/testing': + specifier: 'catalog:' specifier: 'catalog:' version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6) '@types/express': From 9f24e19f1e9f093eeb8ff16cd1aa92471a58d98e Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Wed, 8 Oct 2025 12:42:29 +0530 Subject: [PATCH 04/43] refactor: update oid4vc flows (#1471) * refactor: update oid4vc flows Signed-off-by: Tipu_Singh * refactor: removed commented code Signed-off-by: Tipu_Singh * refactor: changed example Signed-off-by: Tipu_Singh --------- Signed-off-by: Tipu_Singh --- .../dtos/oid4vc-issuer-template.dto.ts | 16 +- .../oid4vc-issuance.controller.ts | 23 +- .../oid4vc-issuance.service.ts | 8 +- .../helpers/credential-sessions.builder.ts | 285 ++++++++++++------ .../libs/helpers/issuer.metadata.ts | 43 +-- .../src/oid4vc-issuance.controller.ts | 12 +- .../src/oid4vc-issuance.service.ts | 20 +- 7 files changed, 257 insertions(+), 150 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 1035166d7..2d5bfca73 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 @@ -79,21 +79,21 @@ export class AppearanceDto { @ApiPropertyOptional({ example: [ { - locale: 'en', - name: 'Birth Certificate', - description: 'Official record of birth', + locale: 'de', + name: 'Geburtsurkunde', + description: 'Offizielle Geburtsbescheinigung', logo: { uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', alt_text: 'abc_logo' } }, { - locale: 'ar', - name: 'شهادة الميلاد', - description: 'سجل رسمي للولادة', + locale: 'en', + name: 'Birth Certificate', + description: 'Official record of birth', logo: { uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', - alt_text: 'شعار abc' + alt_text: 'abc_logo' } } ] @@ -146,7 +146,7 @@ export class CreateCredentialTemplateDto { @ApiPropertyOptional({ description: 'Verifiable Credential Type (required when format is "vc+sd-jwt"; must NOT be provided when format is "mso_mdoc")', - example: 'org.iso.18013.5.1.mDL' + example: 'BirthCertificateCredential-sdjwt' }) @ValidateIf((o: CreateCredentialTemplateDto) => 'vc+sd-jwt' === o.format) @IsString() diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts index f6d29066e..70a86bcbf 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts @@ -135,8 +135,8 @@ export class Oid4vcIssuanceController { return res.status(HttpStatus.CREATED).json(finalResponse); } - @Get('/orgs/:orgId/oid4vc/issuers/:issuerId') - @ApiOperation({ summary: 'Get OID4VC issuer', description: 'Retrieves an OID4VC issuer by issuerId.' }) + @Get('/orgs/:orgId/oid4vc/issuers/:id') + @ApiOperation({ summary: 'Get OID4VC issuer', description: 'Retrieves an OID4VC issuer by id.' }) @ApiResponse({ status: HttpStatus.OK, description: 'Issuer fetched successfully.', type: ApiResponseDto }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @@ -151,17 +151,17 @@ export class Oid4vcIssuanceController { }) ) orgId: string, - @Param('issuerId') - issuerId: string, + @Param('id') + id: string, @Res() res: Response ): Promise { - const oidcIssuer = await this.oid4vcIssuanceService.oidcGetIssuerById(issuerId, orgId); + const oidcIssuer = await this.oid4vcIssuanceService.oidcGetIssuerById(id, orgId); const finalResponse: IResponse = { - statusCode: HttpStatus.CREATED, + statusCode: HttpStatus.OK, message: ResponseMessages.oidcIssuer.success.fetch, data: oidcIssuer }; - return res.status(HttpStatus.CREATED).json(finalResponse); + return res.status(HttpStatus.OK).json(finalResponse); } @Get('/orgs/:orgId/oid4vc/issuers') @@ -194,7 +194,7 @@ export class Oid4vcIssuanceController { return res.status(HttpStatus.OK).json(finalResponse); } - @Delete('/orgs/:orgId/oid4vc/:issuerId') + @Delete('/orgs/:orgId/oid4vc/:id') @ApiOperation({ summary: 'Delete OID4VC issuer', description: 'Deletes an OID4VC issuer for the specified organization.' @@ -213,18 +213,18 @@ export class Oid4vcIssuanceController { ) orgId: string, @Param( - 'issuerId', + 'id', new ParseUUIDPipe({ exceptionFactory: (): Error => { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); } }) ) - issuerId: string, + id: string, @User() user: user, @Res() res: Response ): Promise { - await this.oid4vcIssuanceService.oidcDeleteIssuer(user, orgId, issuerId); + await this.oid4vcIssuanceService.oidcDeleteIssuer(user, orgId, id); const finalResponse: IResponse = { statusCode: HttpStatus.OK, @@ -253,7 +253,6 @@ export class Oid4vcIssuanceController { @Res() res: Response ): Promise { CredentialTemplate.issuerId = issuerId; - console.log('THis is dto', JSON.stringify(CredentialTemplate, null, 2)); const template = await this.oid4vcIssuanceService.createTemplate(CredentialTemplate, user, orgId, issuerId); const finalResponse: IResponse = { diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts index b5282d9dc..57dc99214 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts @@ -37,8 +37,8 @@ export class Oid4vcIssuanceService extends BaseService { return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-issuer-update', payload); } - async oidcGetIssuerById(issuerId: string, orgId: string): Promise { - const payload = { issuerId, orgId }; + async oidcGetIssuerById(id: string, orgId: string): Promise { + const payload = { id, orgId }; return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-issuer-get-by-id', payload); } @@ -47,8 +47,8 @@ export class Oid4vcIssuanceService extends BaseService { return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-get-issuers-issuance', payload); } - async oidcDeleteIssuer(userDetails: user, orgId: string, issuerId: string): Promise { - const payload = { issuerId, orgId, userDetails }; + async oidcDeleteIssuer(userDetails: user, orgId: string, id: string): Promise { + const payload = { id, orgId, userDetails }; return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-delete-issuer', payload); } diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 85cf77e23..b5e353c19 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -1,4 +1,4 @@ -// builder/credential-offer.builder.ts +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ import { Prisma, credential_templates } from '@prisma/client'; import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; @@ -6,7 +6,7 @@ import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-iss Domain Types ============================================================================ */ -type ValueType = 'string' | 'date' | 'number' | 'boolean' | string; +type ValueType = 'string' | 'date' | 'number' | 'boolean' | 'integer' | string; interface TemplateAttribute { display: { name: string; locale: string }[]; @@ -17,7 +17,7 @@ type TemplateAttributes = Record; export enum CredentialFormat { SdJwtVc = 'vc+sd-jwt', - Mdoc = 'mdoc' + Mdoc = 'mso_mdoc' } export enum SignerMethodOption { @@ -105,25 +105,25 @@ export type CredentialOfferPayload = BuiltCredentialOfferBase & Small Utilities ============================================================================ */ -const isNil = (v: unknown): v is null | undefined => null == v; -const isEmptyString = (v: unknown): boolean => 'string' === typeof v && '' === v.trim(); -const isRecord = (v: unknown): v is Record => Boolean(v) && 'object' === typeof v && !Array.isArray(v); +const isNil = (value: unknown): value is null | undefined => null == value; +const isEmptyString = (value: unknown): boolean => 'string' === typeof value && '' === value.trim(); +const isPlainRecord = (value: unknown): value is Record => + Boolean(value) && 'object' === typeof value && !Array.isArray(value); /** Map DB format string -> API enum */ -function mapDbFormatToApiFormat(db: string): CredentialFormat { - const norm = db.toLowerCase().replace(/_/g, '-'); - if ('sd-jwt' === norm || 'vc+sd-jwt' === norm || 'sdjwt' === norm || 'sd+jwt-vc' === norm) { +function mapDbFormatToApiFormat(dbFormat: string): CredentialFormat { + if ('sd-jwt' === dbFormat || 'vc+sd-jwt' === dbFormat || 'sdjwt' === dbFormat || 'sd+jwt-vc' === dbFormat) { return CredentialFormat.SdJwtVc; } - if ('mdoc' === norm || 'mso-mdoc' === norm || 'mso-mdoc' === norm) { + if ('mso_mdoc' === dbFormat) { return CredentialFormat.Mdoc; } - throw new Error(`Unsupported template format: ${db}`); + throw new Error(`Unsupported template format: ${dbFormat}`); } /** Map API enum -> id suffix required for credentialSupportedId */ -function formatSuffix(api: CredentialFormat): 'sdjwt' | 'mdoc' { - return api === CredentialFormat.SdJwtVc ? 'sdjwt' : 'mdoc'; +function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' { + return apiFormat === CredentialFormat.SdJwtVc ? 'sdjwt' : 'mdoc'; } /* ============================================================================ @@ -134,20 +134,20 @@ function formatSuffix(api: CredentialFormat): 'sdjwt' | 'mdoc' { function assertMandatoryClaims( payload: Record, attributes: TemplateAttributes, - ctx: { templateId: string } + context: { templateId: string } ): void { - const missing: string[] = []; - for (const [claim, def] of Object.entries(attributes)) { - if (!def?.mandatory) { + const missingClaims: string[] = []; + for (const [claimName, attributeDefinition] of Object.entries(attributes)) { + if (!attributeDefinition?.mandatory) { continue; } - const val = payload[claim]; - if (isNil(val) || isEmptyString(val)) { - missing.push(claim); + const claimValue = payload[claimName]; + if (isNil(claimValue) || isEmptyString(claimValue)) { + missingClaims.push(claimName); } } - if (missing.length) { - throw new Error(`Missing mandatory claims for template "${ctx.templateId}": ${missing.join(', ')}`); + if (missingClaims.length) { + throw new Error(`Missing mandatory claims for template "${context.templateId}": ${missingClaims.join(', ')}`); } } @@ -155,30 +155,130 @@ function assertMandatoryClaims( JsonValue → TemplateAttributes Narrowing (Type Guards) ============================================================================ */ -function isDisplayArray(v: unknown): v is { name: string; locale: string }[] { - return Array.isArray(v) && v.every((d) => isRecord(d) && 'string' === typeof d.name && 'string' === typeof d.locale); -} - -function isTemplateAttribute(v: unknown): v is TemplateAttribute { +function isDisplayArray(value: unknown): value is { name: string; locale: string }[] { return ( - isRecord(v) && isDisplayArray(v.display) && 'boolean' === typeof v.mandatory && 'string' === typeof v.value_type + Array.isArray(value) && + value.every( + (entry) => + isPlainRecord(entry) && 'string' === typeof (entry as any).name && 'string' === typeof (entry as any).locale + ) ); } -/** Accept `unknown` so predicate type (TemplateAttributes) is assignable to parameter type. */ -function isTemplateAttributes(v: unknown): v is TemplateAttributes { - if (!isRecord(v)) { - return false; +/* ============================================================================ + Improved ensureTemplateAttributes: runtime assert with helpful errors +============================================================================ */ + +const ALLOWED_VALUE_TYPES: ValueType[] = ['string', 'date', 'number', 'boolean', 'integer']; + +function ensureTemplateAttributes(jsonValue: Prisma.JsonValue): TemplateAttributes { + if (!isPlainRecord(jsonValue)) { + throw new Error( + `Invalid template.attributes: expected an object map but received ${ + null === jsonValue ? 'null' : typeof jsonValue + }.\n\nFix: provide an object whose keys are attribute names and whose values are attribute definitions, e.g.\n{\n "given_name": { "mandatory": true, "value_type": "string" }\n}` + ); + } + + const attributesMap = jsonValue as Record; + const attributeKeys = Object.keys(attributesMap); + if (0 === attributeKeys.length) { + throw new Error( + 'Invalid template.attributes: object is empty (no attributes defined).\n\nFix: add at least one attribute definition, for example:\n{\n "given_name": { "mandatory": true, "value_type": "string" }\n}' + ); + } + + const problems: string[] = []; + const suggestedFixes: string[] = []; + + for (const attributeKey of attributeKeys) { + const rawAttributeDef = attributesMap[attributeKey]; + + if (!isPlainRecord(rawAttributeDef)) { + problems.push( + `${attributeKey}: expected an object but got ${null === rawAttributeDef ? 'null' : typeof rawAttributeDef}` + ); + suggestedFixes.push( + `Replace attribute "${attributeKey}" value with an object, e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + continue; + } + + // mandatory checks + if (!('mandatory' in rawAttributeDef)) { + problems.push(`${attributeKey}.mandatory: missing`); + suggestedFixes.push( + `Add mandatory boolean for "${attributeKey}":\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + } else if ('boolean' !== typeof (rawAttributeDef as any).mandatory) { + problems.push(`${attributeKey}.mandatory: expected boolean but got ${typeof (rawAttributeDef as any).mandatory}`); + suggestedFixes.push( + `Set "mandatory" to a boolean for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + } + + // value_type checks + if (!('value_type' in rawAttributeDef)) { + problems.push(`${attributeKey}.value_type: missing`); + suggestedFixes.push( + `Add value_type for "${attributeKey}", for example:\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + } else if ('string' !== typeof (rawAttributeDef as any).value_type) { + problems.push( + `${attributeKey}.value_type: expected string but got ${typeof (rawAttributeDef as any).value_type}` + ); + suggestedFixes.push( + `Make sure "value_type" is a string for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + } else { + const declaredType = (rawAttributeDef as any).value_type as string; + if (!ALLOWED_VALUE_TYPES.includes(declaredType as ValueType)) { + problems.push( + `${attributeKey}.value_type: unsupported value_type "${declaredType}". Allowed types: ${ALLOWED_VALUE_TYPES.join(', ')}` + ); + suggestedFixes.push( + `Use one of the allowed types for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + } + } + + // display checks (optional) + if ('display' in rawAttributeDef && !isDisplayArray((rawAttributeDef as any).display)) { + problems.push(`${attributeKey}.display: expected array of { name: string, locale: string }`); + suggestedFixes.push( + `Fix "display" for "${attributeKey}" to be an array of objects with name/locale, e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string", "display": [{ "name": "Given Name", "locale": "en-US" }] }` + ); + } } - return Object.values(v).every(isTemplateAttribute); -} -/** Runtime assert + narrow Prisma.JsonValue → TemplateAttributes */ -function ensureTemplateAttributes(v: Prisma.JsonValue): TemplateAttributes { - if (!isTemplateAttributes(v)) { - throw new Error('Invalid template.attributes shape. Expecting TemplateAttributes map.'); + if (0 < problems.length) { + // Build a user-friendly message: problems + suggested fixes (unique) + const uniqueFixes = Array.from(new Set(suggestedFixes)).slice(0, 20); + const fixesText = uniqueFixes.length + ? `\n\nSuggested fixes (copy-paste examples):\n- ${uniqueFixes.join('\n- ')}` + : ''; + + // Include a small truncated sample of the attributes to help debugging + const samplePreview = JSON.stringify( + Object.fromEntries(attributeKeys.slice(0, 10).map((key) => [key, attributesMap[key]])), + (_, value) => { + if ('string' === typeof value && 200 < value.length) { + return `${value.slice(0, 200)}...`; + } + return value; + }, + 2 + ); + + throw new Error( + `Invalid template.attributes shape. Problems found:\n- ${problems.join( + '\n- ' + )}\n\nExample attributes (truncated):\n${samplePreview}${fixesText}` + ); } - return v; + + // Safe to cast to TemplateAttributes + return attributesMap as TemplateAttributes; } /* ============================================================================ @@ -187,31 +287,31 @@ function ensureTemplateAttributes(v: Prisma.JsonValue): TemplateAttributes { /** Build one credential block normalized to API format (using the template's format). */ function buildOneCredential( - cred: CredentialRequestDtoLike, - template: credential_templates, - attrs: TemplateAttributes, + credentialRequest: CredentialRequestDtoLike, + templateRecord: credential_templates, + templateAttributes: TemplateAttributes, signerOptions?: SignerOption[] ): BuiltCredential { // 1) Validate payload against template attributes - assertMandatoryClaims(cred.payload, attrs, { templateId: cred.templateId }); + assertMandatoryClaims(credentialRequest.payload, templateAttributes, { templateId: credentialRequest.templateId }); // 2) Decide API format from DB format - const apiFormat = mapDbFormatToApiFormat(template.format); + const selectedApiFormat = mapDbFormatToApiFormat(templateRecord.format); // 3) Build supportedId from template.name + suffix ("-sdjwt" | "-mdoc") - const suffix = formatSuffix(apiFormat); - const credentialSupportedId = `${template.name}-${suffix}`; + const idSuffix = formatSuffix(selectedApiFormat); + const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; // 4) Strip vct ALWAYS (per requirement) - const payload = { ...(cred.payload as Record) }; - delete (payload as Record).vct; + const normalizedPayload = { ...(credentialRequest.payload as Record) }; + delete (normalizedPayload as Record).vct; return { credentialSupportedId, // e.g., "BirthCertificateCredential-sdjwt" - signerOptions: signerOptions[0], - format: apiFormat, // 'vc+sd-jwt' | 'mdoc' - payload, // without vct - ...(cred.disclosureFrame ? { disclosureFrame: cred.disclosureFrame } : {}) + signerOptions: signerOptions ? signerOptions[0] : undefined, + format: selectedApiFormat, // 'vc+sd-jwt' | 'mdoc' + payload: normalizedPayload, // without vct + ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) }; } @@ -231,44 +331,46 @@ export function buildCredentialOfferPayload( signerOptions?: SignerOption[] ): CredentialOfferPayload { // Index templates - const byId = new Map(templates.map((t) => [t.id, t])); + const templatesById = new Map(templates.map((template) => [template.id, template])); // Verify all requested templateIds exist - const unknown = dto.credentials.map((c) => c.templateId).filter((id) => !byId.has(id)); - if (unknown.length) { - throw new Error(`Unknown template ids: ${unknown.join(', ')}`); + const unknownTemplateIds = dto.credentials + .map((c) => c.templateId) + .filter((requestedId) => !templatesById.has(requestedId)); + if (unknownTemplateIds.length) { + throw new Error(`Unknown template ids: ${unknownTemplateIds.join(', ')}`); } // Build credentials - const credentials: BuiltCredential[] = dto.credentials.map((cred) => { - const template = byId.get(cred.templateId)!; - const attrs = ensureTemplateAttributes(template.attributes); // narrow JsonValue safely - return buildOneCredential(cred, template, attrs, signerOptions); + const builtCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { + const templateRecord = templatesById.get(credentialRequest.templateId)!; + const resolvedAttributes = ensureTemplateAttributes(templateRecord.attributes); // narrow JsonValue safely + return buildOneCredential(credentialRequest, templateRecord, resolvedAttributes, signerOptions); }); // --- Base envelope (issuerId deliberately NOT included) --- - const base: BuiltCredentialOfferBase = { - credentials, + const baseEnvelope: BuiltCredentialOfferBase = { + credentials: builtCredentials, ...(dto.publicIssuerId ? { publicIssuerId: dto.publicIssuerId } : {}) }; // XOR flow selection (defensive) - const hasPre = Boolean(dto.preAuthorizedCodeFlowConfig); - const hasAuth = Boolean(dto.authorizationCodeFlowConfig); - if (hasPre === hasAuth) { + const hasPreAuthFlow = Boolean(dto.preAuthorizedCodeFlowConfig); + const hasAuthCodeFlow = Boolean(dto.authorizationCodeFlowConfig); + if (hasPreAuthFlow === hasAuthCodeFlow) { throw new Error('Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'); } - if (hasPre) { + if (hasPreAuthFlow) { return { - ...base, - preAuthorizedCodeFlowConfig: dto.preAuthorizedCodeFlowConfig! // definite since hasPre + ...baseEnvelope, + preAuthorizedCodeFlowConfig: dto.preAuthorizedCodeFlowConfig! }; } return { - ...base, - authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! // definite since !hasPre + ...baseEnvelope, + authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! }; } @@ -280,44 +382,49 @@ export function buildUpdateCredentialOfferPayload( templates: credential_templates[] ): { credentials: BuiltCredential[] } { // Index templates by id - const byId = new Map(templates.map((t) => [t.id, t])); + const templatesById = new Map(templates.map((template) => [template.id, template])); // Validate all templateIds exist - const unknown = dto.credentials.map((c) => c.templateId).filter((id) => !byId.has(id)); - if (unknown.length) { - throw new Error(`Unknown template ids: ${unknown.join(', ')}`); + const unknownTemplateIds = dto.credentials + .map((c) => c.templateId) + .filter((requestedId) => !templatesById.has(requestedId)); + if (unknownTemplateIds.length) { + throw new Error(`Unknown template ids: ${unknownTemplateIds.join(', ')}`); } // Validate each credential against its template - const credentials: BuiltCredential[] = dto.credentials.map((cred) => { - const template = byId.get(cred.templateId)!; - const attrs = ensureTemplateAttributes(template.attributes); // safely narrow JsonValue + const normalizedCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { + const templateRecord = templatesById.get(credentialRequest.templateId)!; + const resolvedAttributes = ensureTemplateAttributes(templateRecord.attributes); // safely narrow JsonValue // check that all payload keys exist in template attributes - const payloadKeys = Object.keys(cred.payload); - const invalidKeys = payloadKeys.filter((k) => !attrs[k]); - if (invalidKeys.length) { - throw new Error(`Invalid attributes for template "${cred.templateId}": ${invalidKeys.join(', ')}`); + const payloadKeys = Object.keys(credentialRequest.payload); + const invalidPayloadKeys = payloadKeys.filter((payloadKey) => !resolvedAttributes[payloadKey]); + if (invalidPayloadKeys.length) { + throw new Error( + `Invalid attributes for template "${credentialRequest.templateId}": ${invalidPayloadKeys.join(', ')}` + ); } // also validate mandatory fields are present - assertMandatoryClaims(cred.payload, attrs, { templateId: cred.templateId }); + assertMandatoryClaims(credentialRequest.payload, resolvedAttributes, { templateId: credentialRequest.templateId }); // build minimal normalized credential (no vct, issuerId, etc.) - const apiFormat = mapDbFormatToApiFormat(template.format); - const suffix = formatSuffix(apiFormat); - const credentialSupportedId = `${template.name}-${suffix}`; + const selectedApiFormat = mapDbFormatToApiFormat(templateRecord.format); + const idSuffix = formatSuffix(selectedApiFormat); + const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + return { credentialSupportedId, - format: apiFormat, - payload: cred.payload, - ...(cred.disclosureFrame ? { disclosureFrame: cred.disclosureFrame } : {}) + format: selectedApiFormat, + payload: credentialRequest.payload, + ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) }; }); // Only return credentials array here (update flow doesn't need preAuth/auth configs) return { - credentials + credentials: normalizedCredentials }; } diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index 22735c10a..4f6bc6047 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -47,7 +47,6 @@ type CredentialConfigurationsSupported = { // ---- Static Lists (as requested) ---- const STATIC_CREDENTIAL_ALGS = ['ES256', 'EdDSA'] as const; const STATIC_BINDING_METHODS = ['did:key'] as const; -const MSO_MDOC = 'mso_mdoc'; // alternative format value // Safe coercion helpers function coerceJsonObject(v: Prisma.JsonValue): T | null { @@ -101,10 +100,10 @@ export function buildCredentialConfigurationsSupported( format?: string; } ): CredentialConfigurationsSupported { - const format = opts?.format ?? 'vc+sd-jwt'; + const defaultFormat = opts?.format ?? 'vc+sd-jwt'; const credentialConfigurationsSupported: Record = {}; + for (const t of templates) { - // Coerce JSON fields const attrs = coerceJsonObject(t.attributes); const app = coerceJsonObject(t.appearance); @@ -115,27 +114,36 @@ export function buildCredentialConfigurationsSupported( throw new Error(`Template ${t.id}: invalid appearance JSON (missing display)`); } - // ---- dynamic format per row ---- + // per-row format (allow column override) // eslint-disable-next-line @typescript-eslint/no-explicit-any - const rowFormat = (t as any).format ?? format; - const isMdoc = rowFormat === `${MSO_MDOC}`; - const suffix = rowFormat === `${MSO_MDOC}` ? 'mdoc' : 'sdjwt'; + const rowFormat: string = (t as any).format ?? defaultFormat; + const isMdoc = 'mso_mdoc' === rowFormat; + const suffix = isMdoc ? 'mdoc' : 'sdjwt'; - // key: keep your keyResolver override; otherwise include suffix + // key (allow override) const key = 'function' === typeof opts?.keyResolver ? opts.keyResolver(t) : `${t.name}-${suffix}`; - // vct/scope: vct only for non-mdoc; scope always uses suffix + // Resolve doctype/vct: + // - For mdoc: try opts.doctype -> t.doctype -> fallback to t.name (or throw if you prefer) + // - For sd-jwt: try opts.vct -> t.vct -> fallback to t.name // eslint-disable-next-line @typescript-eslint/no-explicit-any - const rowDoctype: string | undefined = opts?.doctype ?? (t as any).doctype; + let rowDoctype: string | undefined = opts?.doctype ?? (t as any).doctype; // eslint-disable-next-line @typescript-eslint/no-explicit-any const rowVct: string = opts?.vct ?? (t as any).vct ?? t.name; - if (isMdoc && !rowDoctype) { - throw new Error(`Template ${t.id}: doctype is required for mdoc format`); + if (isMdoc) { + if (!rowDoctype) { + // Fallback strategy: use template's name as doctype (change to throw if you want strictness) + rowDoctype = t.name; + // If you want to fail instead of fallback, uncomment next line: + // throw new Error(`Template ${t.id}: doctype is required for mdoc format`); + } } - const scopeBase = opts?.scopeVct ?? rowVct; + // Choose scope base: prefer opts.scopeVct, otherwise for mdoc use doctype, else vct + const scopeBase = opts?.scopeVct ?? (isMdoc ? rowDoctype : rowVct); const scope = `openid4vc:credential:${scopeBase}-${suffix}`; + const claims = Object.fromEntries( Object.entries(attrs).map(([claimName, def]) => { const d = def as AttributeDef; @@ -143,7 +151,7 @@ export function buildCredentialConfigurationsSupported( claimName, { value_type: d.value_type, - mandatory: d.mandatory ?? false, // always include, default to false + mandatory: d.mandatory ?? false, display: Array.isArray(d.display) ? d.display.map((x) => ({ name: x.name, locale: x.locale })) : undefined } ]; @@ -151,13 +159,12 @@ export function buildCredentialConfigurationsSupported( ); const display = - app.display.map((d) => ({ + app.display?.map((d) => ({ name: d.name, description: d.description, locale: d.locale })) ?? []; - // assemble per-template config credentialConfigurationsSupported[key] = { format: rowFormat, scope, @@ -169,9 +176,7 @@ export function buildCredentialConfigurationsSupported( }; } - return { - credentialConfigurationsSupported - }; + return { credentialConfigurationsSupported }; } // Default DPoP list for issuer-level metadata (match your example) diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts index 7d9f476f7..72a7f4c46 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts @@ -38,12 +38,12 @@ export class Oid4vcIssuanceController { @MessagePattern({ cmd: 'oid4vc-issuer-get-by-id' }) async oidcGetIssuerById(payload: { - issuerId: string; + id: string; orgId: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any }): Promise { - const { issuerId, orgId } = payload; - return this.oid4vcIssuanceService.oidcIssuerGetById(issuerId, orgId); + const { id, orgId } = payload; + return this.oid4vcIssuanceService.oidcIssuerGetById(id, orgId); } @MessagePattern({ cmd: 'oid4vc-get-issuers-issuance' }) @@ -56,11 +56,11 @@ export class Oid4vcIssuanceController { async deleteOidcIssuer(payload: { orgId: string; userDetails: user; - issuerId: string; + id: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any }): Promise { - const { orgId, userDetails, issuerId } = payload; - return this.oid4vcIssuanceService.deleteOidcIssuer(orgId, userDetails, issuerId); + const { orgId, userDetails, id } = payload; + return this.oid4vcIssuanceService.deleteOidcIssuer(orgId, userDetails, id); } @MessagePattern({ cmd: 'oid4vc-template-create' }) diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index afdf46be6..676747ecc 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -188,10 +188,9 @@ export class Oid4vcIssuanceService { } } - async oidcIssuerGetById(issuerId: string, orgId: string): Promise { + async oidcIssuerGetById(id: string, orgId: string): Promise { try { - const getIssuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); - console.log('This is the getIssuerDetails:', JSON.stringify(getIssuerDetails, null, 2)); + const getIssuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(id); if (!getIssuerDetails && getIssuerDetails.publicIssuerId) { throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); } @@ -199,12 +198,9 @@ export class Oid4vcIssuanceService { if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } - console.log('This is the agentDetails:', getIssuerDetails?.publicIssuerId); const encodedId = encodeIssuerPublicId(getIssuerDetails?.publicIssuerId); const url = await getAgentUrl(agentDetails?.agentEndPoint, CommonConstants.OIDC_ISSUER_BY_ID, encodedId); - console.log('This is the oidcIssuerGetById url:', url); const issuerDetailsRaw = await this._oidcGetIssuerById(url, orgId); - console.log('This is the issuerDetailsRaw:', JSON.stringify(issuerDetailsRaw, null, 2)); if (!issuerDetailsRaw) { throw new InternalServerErrorException(`Error from agent while getting issuer`); } @@ -254,13 +250,9 @@ export class Oid4vcIssuanceService { } } - async deleteOidcIssuer(orgId: string, userDetails: user, issuerId: string) { + async deleteOidcIssuer(orgId: string, userDetails: user, id: string) { try { - const deleteOidcIssuer = await this.oid4vcIssuanceRepository.deleteOidcIssuer(issuerId); - if (!deleteOidcIssuer) { - throw new NotFoundException(ResponseMessages.oidcIssuer.error.deleteFailed); - } - const issuerRecordId = await this.oidcIssuerGetById(issuerId, orgId); + const issuerRecordId = await this.oidcIssuerGetById(id, orgId); if (!issuerRecordId.id) { throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); } @@ -274,6 +266,10 @@ export class Oid4vcIssuanceService { if (!createTemplateOnAgent) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } + const deleteOidcIssuer = await this.oid4vcIssuanceRepository.deleteOidcIssuer(id); + if (!deleteOidcIssuer) { + throw new NotFoundException(ResponseMessages.oidcIssuer.error.deleteFailed); + } return deleteOidcIssuer; } catch (error) { if ('PrismaClientKnownRequestError' === error.name) { From c2c8a2ab83979ecf0bfee7edd5b4afe7e4ab49b3 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Wed, 15 Oct 2025 12:29:35 +0530 Subject: [PATCH 05/43] Initial changes for x509 support (#1482) Signed-off-by: Rinkal Bhojani --- .env.demo | 1 + .env.sample | 1 + .../src/agent-service.controller.ts | 32 ++ .../src/agent-service.service.ts | 53 ++- apps/api-gateway/src/app.module.ts | 4 +- .../dtos/issuer-sessions.dto.ts | 6 - .../oid4vc-issuance.service.ts | 6 +- apps/api-gateway/src/x509/dtos/x509.dto.ts | 382 ++++++++++++++++++ apps/api-gateway/src/x509/x509.controller.ts | 279 +++++++++++++ apps/api-gateway/src/x509/x509.module.ts | 29 ++ apps/api-gateway/src/x509/x509.service.ts | 88 ++++ .../helpers/credential-sessions.builder.ts | 6 +- apps/oid4vc-issuance/src/main.ts | 2 +- apps/x509/src/interfaces/x509.interface.ts | 53 +++ apps/x509/src/main.ts | 21 + apps/x509/src/repositories/x509.repository.ts | 326 +++++++++++++++ apps/x509/src/x509.controller.ts | 58 +++ apps/x509/src/x509.module.ts | 31 ++ apps/x509/src/x509.service.ts | 346 ++++++++++++++++ apps/x509/tsconfig.app.json | 9 + libs/common/src/common.constant.ts | 13 +- libs/common/src/common.utils.ts | 6 +- libs/common/src/interfaces/x509.interface.ts | 246 +++++++++++ libs/common/src/response-messages/index.ts | 22 + libs/enum/src/enum.ts | 40 +- .../migration.sql | 58 +++ libs/prisma-service/prisma/schema.prisma | 22 + nest-cli.json | 9 + 28 files changed, 2128 insertions(+), 21 deletions(-) create mode 100644 apps/api-gateway/src/x509/dtos/x509.dto.ts create mode 100644 apps/api-gateway/src/x509/x509.controller.ts create mode 100644 apps/api-gateway/src/x509/x509.module.ts create mode 100644 apps/api-gateway/src/x509/x509.service.ts create mode 100644 apps/x509/src/interfaces/x509.interface.ts create mode 100644 apps/x509/src/main.ts create mode 100644 apps/x509/src/repositories/x509.repository.ts create mode 100644 apps/x509/src/x509.controller.ts create mode 100644 apps/x509/src/x509.module.ts create mode 100644 apps/x509/src/x509.service.ts create mode 100644 apps/x509/tsconfig.app.json create mode 100644 libs/common/src/interfaces/x509.interface.ts create mode 100644 libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql diff --git a/.env.demo b/.env.demo index c9cd8c829..afbce9304 100644 --- a/.env.demo +++ b/.env.demo @@ -102,6 +102,7 @@ UTILITIES_NKEY_SEED= CLOUD_WALLET_NKEY_SEED= GEOLOCATION_NKEY_SEED= NOTIFICATION_NKEY_SEED= +X509_NKEY_SEED= KEYCLOAK_DOMAIN=http://localhost:8080/ KEYCLOAK_ADMIN_URL=http://localhost:8080 diff --git a/.env.sample b/.env.sample index cb422b084..7e5397508 100644 --- a/.env.sample +++ b/.env.sample @@ -121,6 +121,7 @@ CREDENTAILDEFINITION_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for SCHEMA_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for schema service UTILITIES_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for utilities service GEOLOCATION_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for geo-location service +X509_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for x509 service AFJ_AGENT_TOKEN_PATH=/apps/agent-provisioning/AFJ/token/ diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index 0c4d7c81b..e0f82ce20 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -27,6 +27,11 @@ import { user } from '@prisma/client'; import { InvitationMessage } from '@credebl/common/interfaces/agent-service.interface'; import { AgentSpinUpStatus } from '@credebl/enum/enum'; import { SignDataDto } from '../../api-gateway/src/agent-service/dto/agent-service.dto'; +import { + IX509ImportCertificateOptionsDto, + x509CertificateDecodeDto, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; @Controller() export class AgentServiceController { @@ -380,4 +385,31 @@ export class AgentServiceController { async oidcDeleteCredentialOffer(payload: { url: string; orgId: string }): Promise { return this.agentServiceService.oidcDeleteCredentialOffer(payload.url, payload.orgId); } + + @MessagePattern({ cmd: 'agent-create-x509-certificate' }) + async createX509Certificate(payload: { + options: X509CreateCertificateOptions; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.createX509Certificate(payload.options, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-decode-x509-certificate' }) + async decodeX509Certificate(payload: { + options: x509CertificateDecodeDto; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.decodeX509Certificate(payload.options, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-import-x509-certificate' }) + async importX509Certificate(payload: { + options: IX509ImportCertificateOptionsDto; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.importX509Certificate(payload.options, payload.url, payload.orgId); + } } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index e9c0c08ad..ec8e3c26b 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -63,7 +63,6 @@ import { Prisma, RecordType, ledgers, org_agents, organisation, platform_config, import { CommonConstants } from '@credebl/common/common.constant'; import { CommonService } from '@credebl/common'; import { GetSchemaAgentRedirection } from 'apps/ledger/src/schema/schema.interface'; -import { ConnectionService } from 'apps/connection/src/connection.service'; import { ResponseMessages } from '@credebl/common/response-messages'; import { Socket, io } from 'socket.io-client'; import { WebSocketGateway } from '@nestjs/websockets'; @@ -80,6 +79,11 @@ import { NATSClient } from '@credebl/common/NATSClient'; import { SignDataDto } from '../../api-gateway/src/agent-service/dto/agent-service.dto'; import { IVerificationMethod } from 'apps/organization/interfaces/organization.interface'; import { getAgentUrl } from '@credebl/common/common.utils'; +import { + IX509ImportCertificateOptionsDto, + x509CertificateDecodeDto, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; @Injectable() @WebSocketGateway() export class AgentServiceService { @@ -89,8 +93,6 @@ export class AgentServiceService { private readonly agentServiceRepository: AgentServiceRepository, private readonly prisma: PrismaService, private readonly commonService: CommonService, - // TODO: Remove duplicate, unused variable - private readonly connectionService: ConnectionService, @Inject('NATS_CLIENT') private readonly agentServiceProxy: ClientProxy, // TODO: Remove duplicate, unused variable @Inject(CACHE_MANAGER) private cacheService: Cache, @@ -2223,4 +2225,49 @@ export class AgentServiceService { throw error; } } + + async createX509Certificate(options: X509CreateCertificateOptions, url: string, orgId: string): Promise { + try { + this.logger.log('Start creating X509 certificate'); + this.logger.debug('Creating X509 certificate with options', options); + const getApiKey = await this.getOrgAgentApiKey(orgId); + const x509Certificate = await this.commonService + .httpPost(url, options, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return x509Certificate; + } catch (error) { + this.logger.error(`Error in creating x509 certificate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async decodeX509Certificate(options: x509CertificateDecodeDto, url: string, orgId: string): Promise { + try { + this.logger.log('Start decoding X509 certificate'); + this.logger.debug('Decoding X509 certificate with options', options); + const getApiKey = await this.getOrgAgentApiKey(orgId); + const x509Certificate = await this.commonService + .httpPost(url, options, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return x509Certificate; + } catch (error) { + this.logger.error(`Error in decoding x509 certificate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async importX509Certificate(options: IX509ImportCertificateOptionsDto, url: string, orgId: string): Promise { + try { + this.logger.log('Start importing X509 certificate'); + this.logger.debug(`Importing X509 certificate with options`, options.certificate); + const getApiKey = await this.getOrgAgentApiKey(orgId); + const x509Certificate = await this.commonService + .httpPost(url, options, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return x509Certificate; + } catch (error) { + this.logger.error(`Error in creating x509 certificate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } } diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 287ac36ec..370c3e381 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -31,6 +31,7 @@ import { LoggerModule } from '@credebl/logger/logger.module'; import { GlobalConfigModule } from '@credebl/config/global-config.module'; import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; +import { X509Module } from './x509/x509.module'; @Module({ imports: [ @@ -64,7 +65,8 @@ import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; CacheModule.register(), GeoLocationModule, CloudWalletModule, - Oid4vcIssuanceModule + Oid4vcIssuanceModule, + X509Module ], controllers: [AppController], providers: [ 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 4d056d2fc..77c88b075 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 @@ -22,12 +22,6 @@ import { import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -/* ========= Enums ========= */ -export enum CredentialFormat { - SdJwtVc = 'vc+sd-jwt', - Mdoc = 'mdoc' -} - /* ========= disclosureFrame custom validator ========= */ function isDisclosureFrameValue(v: unknown): boolean { if ('boolean' === typeof v) { diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts index 57dc99214..b4da516ab 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts @@ -7,6 +7,7 @@ import { BaseService } from 'libs/service/base.service'; import { oidc_issuer, user } from '@prisma/client'; import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oid4vc-issuer-template.dto'; import { + CreateCredentialOfferD2ADto, CreateOidcCredentialOfferDto, GetAllCredentialOfferDto, UpdateCredentialRequestDto @@ -98,7 +99,10 @@ export class Oid4vcIssuanceService extends BaseService { return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-create-credential-offer', payload); } - async createOidcCredentialOfferD2A(oidcCredentialD2APayload, orgId: string): Promise { + async createOidcCredentialOfferD2A( + oidcCredentialD2APayload: CreateCredentialOfferD2ADto, + orgId: string + ): Promise { const payload = { oidcCredentialD2APayload, orgId }; return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-create-credential-offer-D2A', payload); } diff --git a/apps/api-gateway/src/x509/dtos/x509.dto.ts b/apps/api-gateway/src/x509/dtos/x509.dto.ts new file mode 100644 index 000000000..23548ecdb --- /dev/null +++ b/apps/api-gateway/src/x509/dtos/x509.dto.ts @@ -0,0 +1,382 @@ +import { ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; +import { + ArrayNotEmpty, + IsArray, + IsBoolean, + IsDate, + IsEnum, + IsNotEmpty, + IsNotEmptyObject, + IsNumber, + IsOptional, + IsString, + Max, + Min, + ValidateNested +} from 'class-validator'; +import { Transform, Type } from 'class-transformer'; +import { SortFields, X509ExtendedKeyUsage, X509KeyUsage, x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; +import { IX509SearchCriteria } from '@credebl/common/interfaces/x509.interface'; +import { toNumber, trim } from '@credebl/common/cast.helper'; + +export class AuthorityAndSubjectKeyDto { + @ApiProperty({ + enum: x5cKeyType, + //default: x5cKeyType.P256.toString(), + description: 'Type of the key used for signing the X.509 Certificate (default is p256)' + }) + @IsOptional() + @IsEnum(x5cKeyType) + keyType: x5cKeyType = x5cKeyType.P256; +} + +export enum GeneralNameType { + DNS = 'dns', + DN = 'dn', + EMAIL = 'email', + GUID = 'guid', + IP = 'ip', + URL = 'url', + UPN = 'upn', + REGISTERED_ID = 'id' +} + +export class NameDto { + @ApiProperty({ + example: Object.keys(GeneralNameType), + enum: GeneralNameType + }) + @IsNotEmpty() + @IsEnum(GeneralNameType) + type: GeneralNameType; + + @ApiProperty() + @IsNotEmpty() + @IsString() + value: string; +} + +export class X509CertificateIssuerAndSubjectOptionsDto { + @ApiPropertyOptional() @IsOptional() @IsString() countryName?: string; + @ApiPropertyOptional() @IsOptional() @IsString() stateOrProvinceName?: string; + @ApiPropertyOptional() @IsOptional() @IsString() organizationalUnit?: string; + @ApiPropertyOptional() @IsOptional() @IsString() commonName?: string; +} + +class ValidityDto { + @ApiPropertyOptional() @IsOptional() @IsDate() @Type(() => Date) notBefore?: Date; + @ApiPropertyOptional() @IsOptional() @IsDate() @Type(() => Date) notAfter?: Date; +} + +export class KeyUsageDto { + @ApiProperty({ + enum: X509KeyUsage, + isArray: true, + example: Object.keys(X509KeyUsage) + }) + @IsArray() + @IsEnum(X509KeyUsage, { each: true }) + usages: X509KeyUsage[]; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + markAsCritical?: boolean; +} + +export class ExtendedKeyUsageDto { + @ApiProperty({ + enum: X509ExtendedKeyUsage, + isArray: true, + example: Object.keys(X509ExtendedKeyUsage) + }) + @IsArray() + @IsEnum(X509ExtendedKeyUsage, { each: true }) + usages: X509ExtendedKeyUsage[]; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + markAsCritical?: boolean; +} + +export class NameListDto { + @ApiProperty({ type: [NameDto] }) + @ArrayNotEmpty() + @IsArray() + @ValidateNested() + @Type(() => NameDto) + name: NameDto[]; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + markAsCritical?: boolean; +} + +export class AuthorityAndSubjectKeyIdentifierDto { + @ApiPropertyOptional() + @IsBoolean() + include: boolean; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + markAsCritical?: boolean; +} + +export class BasicConstraintsDto { + @ApiProperty() + @IsBoolean() + ca: boolean; + + @ApiPropertyOptional() + @IsNumber() + @IsOptional() + pathLenConstraint?: number; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + markAsCritical?: boolean; +} + +export class CrlDistributionPointsDto { + @ApiProperty({ type: [String] }) + @IsArray() + @IsString({ each: true }) + urls: string[]; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + markAsCritical?: boolean; +} + +export class X509CertificateExtensionsOptionsDto { + @ApiPropertyOptional({ type: KeyUsageDto }) + @IsOptional() + @ValidateNested() + @Type(() => KeyUsageDto) + keyUsage?: KeyUsageDto; + + @ApiPropertyOptional({ type: ExtendedKeyUsageDto }) + @IsOptional() + @ValidateNested() + @Type(() => ExtendedKeyUsageDto) + extendedKeyUsage?: ExtendedKeyUsageDto; + + @ApiPropertyOptional({ type: AuthorityAndSubjectKeyIdentifierDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyIdentifierDto) + authorityKeyIdentifier?: AuthorityAndSubjectKeyIdentifierDto; + + @ApiPropertyOptional({ type: AuthorityAndSubjectKeyIdentifierDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyIdentifierDto) + subjectKeyIdentifier?: AuthorityAndSubjectKeyIdentifierDto; + + @ApiPropertyOptional({ type: NameListDto }) + @IsOptional() + @ValidateNested() + @Type(() => NameListDto) + issuerAlternativeName?: NameListDto; + + @ApiPropertyOptional({ type: NameListDto }) + @IsOptional() + @ValidateNested() + @Type(() => NameListDto) + subjectAlternativeName?: NameListDto; + + @ApiPropertyOptional({ type: BasicConstraintsDto }) + @IsOptional() + @ValidateNested() + @Type(() => BasicConstraintsDto) + basicConstraints?: BasicConstraintsDto; + + @ApiPropertyOptional({ type: CrlDistributionPointsDto }) + @IsOptional() + @ValidateNested() + @Type(() => CrlDistributionPointsDto) + crlDistributionPoints?: CrlDistributionPointsDto; +} + +// Main DTO +//@ApiExtraModels(X509CertificateIssuerAndSubjectOptionsDto) +export class X509CreateCertificateOptionsDto { + @ApiPropertyOptional({ type: () => AuthorityAndSubjectKeyDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyDto) + authorityKey?: AuthorityAndSubjectKeyDto; + + /** + * + * The key that is the subject of the X.509 Certificate + * + * If the `subjectPublicKey` is not included, the `authorityKey` will be used. + * This means that the certificate is self-signed + * + */ + @ApiPropertyOptional({ type: () => AuthorityAndSubjectKeyDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyDto) + subjectPublicKey?: AuthorityAndSubjectKeyDto; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + serialNumber?: string; + + @ApiProperty({ oneOf: [{ $ref: getSchemaPath(X509CertificateIssuerAndSubjectOptionsDto) }, { type: 'string' }] }) + // @ApiProperty({ type: X509CertificateIssuerAndSubjectOptionsDto }) + @ValidateNested() + @Type(() => X509CertificateIssuerAndSubjectOptionsDto) + issuer: X509CertificateIssuerAndSubjectOptionsDto | string; + + @ApiPropertyOptional({ + oneOf: [{ $ref: getSchemaPath(X509CertificateIssuerAndSubjectOptionsDto) }, { type: 'string' }] + }) + @IsOptional() + subject?: X509CertificateIssuerAndSubjectOptionsDto | string; + + @ApiPropertyOptional({ type: () => ValidityDto }) + @IsOptional() + @ValidateNested() + @Type(() => ValidityDto) + validity?: ValidityDto; + + @ApiPropertyOptional({ type: () => X509CertificateExtensionsOptionsDto }) + @IsOptional() + @ValidateNested() + @Type(() => X509CertificateExtensionsOptionsDto) + extensions?: X509CertificateExtensionsOptionsDto; +} + +export class X509ImportCertificateOptionsDto { + @ApiProperty({ + description: 'certificate', + required: true + }) + @IsString() + certificate: string; + + @ApiPropertyOptional({ + description: 'Private key in base64 string format' + }) + @IsOptional() + @IsString() + privateKey?: string; + + @ApiProperty({ + enum: x5cKeyType, + //default: x5cKeyType.P256.toString(), + description: 'Type of the key used for signing the X.509 Certificate (default is p256)' + }) + @IsOptional() + @IsEnum(x5cKeyType) + keyType: x5cKeyType = x5cKeyType.P256; +} + +export class x509Input { + @ApiProperty({ + description: 'certificate', + required: true + }) + @IsString() + certificate: string; +} + +export class X509CertificateSubjectOptionsDto { + @ApiProperty() @IsNotEmpty() @IsString() countryName: string; + // @ApiPropertyOptional() @IsOptional() @IsString() stateOrProvinceName?: string; + // @ApiPropertyOptional() @IsOptional() @IsString() organizationalUnit?: string; + @ApiProperty() @IsNotEmpty() @IsString() commonName: string; +} + +export class BasicX509CreateCertificateConfig { + @ApiProperty({ type: () => X509CertificateSubjectOptionsDto, required: true }) + @IsNotEmptyObject() + @ValidateNested() + @Type(() => X509CertificateSubjectOptionsDto) + subject: X509CertificateSubjectOptionsDto; + + @ApiPropertyOptional({ type: () => AuthorityAndSubjectKeyDto }) + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AuthorityAndSubjectKeyDto) + subjectKey?: AuthorityAndSubjectKeyDto; +} + +export interface X509GenericRecordContent { + dcs?: string | string[]; + root?: string; +} + +export interface X509GenericRecord { + id: string; + content?: X509GenericRecordContent; +} + +export class x509OptionsDto { + @ApiProperty({ example: 'exampleOrg' }) + @IsNotEmpty() + @IsString() + commonName: string; + + @ApiProperty({ example: 'IN' }) + @IsNotEmpty() + @IsString() + countryName: string; +} + +export class X509SearchCriteriaDto implements IX509SearchCriteria { + @ApiProperty({ required: false, example: '1' }) + @Transform(({ value }) => toNumber(value)) + @IsOptional() + pageNumber: number = 1; + + @ApiProperty({ required: false, example: '10' }) + @IsOptional() + @Transform(({ value }) => toNumber(value)) + @Min(1, { message: 'Page size must be greater than 0' }) + @Max(100, { message: 'Page size must be less than 100' }) + pageSize: number = 10; + + @ApiProperty({ required: false }) + @IsOptional() + @Transform(({ value }) => trim(value)) + @Type(() => String) + searchByText: string = ''; + + @ApiProperty({ + required: false + }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsEnum(SortFields) + sortField: string = SortFields.CREATED_DATE_TIME; + + @ApiProperty({ + required: false, + enum: x5cKeyType, + enumName: 'keyType' + }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsEnum(x5cKeyType) + keyType: x5cKeyType; + + @ApiProperty({ + required: false, + enum: x5cRecordStatus, + enumName: 'status' + }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsEnum(x5cRecordStatus) + status: x5cRecordStatus; +} diff --git a/apps/api-gateway/src/x509/x509.controller.ts b/apps/api-gateway/src/x509/x509.controller.ts new file mode 100644 index 000000000..e413071fa --- /dev/null +++ b/apps/api-gateway/src/x509/x509.controller.ts @@ -0,0 +1,279 @@ +import { + ApiBearerAuth, + ApiForbiddenResponse, + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, + ApiUnauthorizedResponse +} from '@nestjs/swagger'; +import { CommonService } from '@credebl/common'; +import { + Controller, + Get, + Put, + Param, + UseGuards, + UseFilters, + Post, + Body, + Res, + HttpStatus, + Query, + ParseUUIDPipe, + BadRequestException +} from '@nestjs/common'; + +import IResponse from '@credebl/common/interfaces/response.interface'; +import { Response } from 'express'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { AuthGuard } from '@nestjs/passport'; +import { User } from '../authz/decorators/user.decorator'; +import { user } from '@prisma/client'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; + +import { TrimStringParamPipe } from '@credebl/common/cast.helper'; +import { X509Service } from './x509.service'; +import { + X509CreateCertificateOptionsDto, + X509ImportCertificateOptionsDto, + X509SearchCriteriaDto +} from './dtos/x509.dto'; +import { SortFields, x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; + +@UseFilters(CustomExceptionFilter) +@Controller('x509') +@ApiTags('x509') +@ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) +export class X509Controller { + constructor( + private readonly x509Service: X509Service, + private readonly commonService: CommonService + ) {} + + /** + * Create a new x509 + * @param createDto The details of the x509 to be created + * @returns Created x509 details + */ + @Post('/:orgId') + @ApiOperation({ + summary: 'Create a new X509', + description: 'Create a new x509 with the provided details.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async createX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Body() createDto: X509CreateCertificateOptionsDto, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.createX509(orgId, createDto, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.x509.success.create, + data: record + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Put('/:orgId/activate/:id') + @ApiOperation({ + summary: 'Activate X509 certificate', + description: 'Activate X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async activateX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id', ParseUUIDPipe) id: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.activateX509(orgId, id, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.activated, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Put('/:orgId/deactivate/:id') + @ApiOperation({ + summary: 'Deactive X509 certificate', + description: 'Deactive X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async deActivateX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id', ParseUUIDPipe) id: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.deActivateX509(orgId, id, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.deActivated, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/:orgId') + @ApiOperation({ + summary: 'Get all X509 certificate', + description: 'Get all X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + @ApiQuery({ + name: 'keyType', + enum: x5cKeyType, + required: false + }) + @ApiQuery({ + name: 'status', + enum: x5cRecordStatus, + required: false + }) + @ApiQuery({ + name: 'sortField', + enum: SortFields, + required: false + }) + async getAllX509ByOrgId( + @Query() x509SearchCriteriaDto: X509SearchCriteriaDto, + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.getX509CertificatesByOrgId(orgId, x509SearchCriteriaDto, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.fetchAll, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/:orgId/:id') + @ApiOperation({ + summary: 'Get X509 certificate', + description: 'Get X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async getX509Certificate( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id', ParseUUIDPipe) id: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.getX509Certificate(orgId, id, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.fetch, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Import a new x509 + * @param importDto The details of the x509 to be created + * @returns Imported x509 certificate + */ + @Post('/:orgId/import') + @ApiOperation({ + summary: 'Import a new X509', + description: 'Import a new x509 with the provided details.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async importX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Body() importDto: X509ImportCertificateOptionsDto, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.importX509(orgId, importDto, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.x509.success.import, + data: record + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } +} diff --git a/apps/api-gateway/src/x509/x509.module.ts b/apps/api-gateway/src/x509/x509.module.ts new file mode 100644 index 000000000..7a4394ca1 --- /dev/null +++ b/apps/api-gateway/src/x509/x509.module.ts @@ -0,0 +1,29 @@ +import { CommonModule, CommonService } from '@credebl/common'; + +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { ConfigModule } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { X509Controller } from './x509.controller'; +import { X509Service } from './x509.service'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { AwsService } from '@credebl/aws'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { NATSClient } from '@credebl/common/NATSClient'; +@Module({ + imports: [ + HttpModule, + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.API_GATEWAY_NKEY_SEED) + }, + CommonModule + ]) + ], + controllers: [X509Controller], + providers: [X509Service, CommonService, AwsService, NATSClient] +}) +export class X509Module {} diff --git a/apps/api-gateway/src/x509/x509.service.ts b/apps/api-gateway/src/x509/x509.service.ts new file mode 100644 index 000000000..e3d82046d --- /dev/null +++ b/apps/api-gateway/src/x509/x509.service.ts @@ -0,0 +1,88 @@ +import { Inject } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; + +import { user } from '@prisma/client'; + +import { NATSClient } from '@credebl/common/NATSClient'; +import { + X509CreateCertificateOptionsDto, + X509ImportCertificateOptionsDto, + X509SearchCriteriaDto +} from './dtos/x509.dto'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; + +@Injectable() +export class X509Service extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy, + private readonly natsClient: NATSClient + ) { + super('X509Service'); + } + + /** + * + * @param createDto + * @returns X509 creation Success + */ + async createX509( + orgId: string, + createDto: X509CreateCertificateOptionsDto, + reqUser: user + ): Promise { + this.logger.log(`Start creating x509 certficate`); + this.logger.debug(`payload : `, createDto, reqUser); + const payload = { options: createDto, user: reqUser, orgId }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'create-x509-certificate', payload); + } + + async activateX509(orgId: string, id: string, reqUser: user): Promise { + this.logger.log(`Start activating x509 certficate`); + this.logger.debug(`certificate Id : `, id); + const payload = { orgId, id, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'activate-x509-certificate', payload); + } + + async deActivateX509(orgId: string, id: string, reqUser: user): Promise { + this.logger.log(`Start deactivating x509 certficate`); + this.logger.debug(`certificate Id : `, id); + const payload = { orgId, id, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'deActivate-x509-certificate', payload); + } + + async getX509CertificatesByOrgId( + orgId: string, + x509SearchCriteriaDto: X509SearchCriteriaDto, + reqUser: user + ): Promise { + this.logger.log(`Start getting x509 certficate for org`); + this.logger.debug(`Filters applied : `, x509SearchCriteriaDto); + const payload = { orgId, options: x509SearchCriteriaDto, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-all-certificates', payload); + } + + async getX509Certificate(orgId: string, id: string, reqUser: user): Promise { + this.logger.log(`Start getting x509 certficate by id`); + this.logger.debug(`certificate Id : `, id); + const payload = { id, orgId, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-certificate', payload); + } + + /** + * + * @param importDto + * @returns X509 import Success + */ + async importX509( + orgId: string, + importDto: X509ImportCertificateOptionsDto, + reqUser: user + ): Promise { + this.logger.log(`Start importing x509 certficate by id`); + this.logger.debug(`certificate : `, importDto.certificate); + const payload = { orgId, options: importDto, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'import-x509-certificate', payload); + } +} diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index b5e353c19..b765bdf5a 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ import { Prisma, credential_templates } from '@prisma/client'; import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; +import { CredentialFormat } from '@credebl/enum/enum'; /* ============================================================================ Domain Types ============================================================================ */ @@ -15,11 +16,6 @@ interface TemplateAttribute { } type TemplateAttributes = Record; -export enum CredentialFormat { - SdJwtVc = 'vc+sd-jwt', - Mdoc = 'mso_mdoc' -} - export enum SignerMethodOption { DID = 'did', X5C = 'x5c' diff --git a/apps/oid4vc-issuance/src/main.ts b/apps/oid4vc-issuance/src/main.ts index 3c6933fc9..a76e0de0f 100644 --- a/apps/oid4vc-issuance/src/main.ts +++ b/apps/oid4vc-issuance/src/main.ts @@ -12,7 +12,7 @@ const logger = new Logger(); async function bootstrap(): Promise { const app = await NestFactory.createMicroservice(Oid4vcIssuanceModule, { transport: Transport.NATS, - options: getNatsOptions(CommonConstants.ISSUANCE_SERVICE, process.env.ISSUANCE_NKEY_SEED) + options: getNatsOptions(CommonConstants.OIDC4VC_ISSUANCE_SERVICE, process.env.ISSUANCE_NKEY_SEED) }); app.useLogger(app.get(NestjsLoggerServiceAdapter)); app.useGlobalFilters(new HttpExceptionFilter()); diff --git a/apps/x509/src/interfaces/x509.interface.ts b/apps/x509/src/interfaces/x509.interface.ts new file mode 100644 index 000000000..bfae58ad9 --- /dev/null +++ b/apps/x509/src/interfaces/x509.interface.ts @@ -0,0 +1,53 @@ +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; +import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; + +export interface CreateX509CertificateEntity { + orgId: string; // We'll accept orgId and find orgAgent internally + keyType: x5cKeyType; + status: string; + validFrom: Date; + expiry: Date; + certificateBase64: string; + createdBy: string; + lastChangedBy: string; +} + +export interface UpdateCertificateStatusDto { + status: x5cRecordStatus; + lastChangedBy: string; +} + +export interface CertificateDateCheckDto { + orgId: string; + validFrom: Date; + expiry: Date; + keyType: x5cKeyType; + status: x5cRecordStatus; + excludeCertificateId?: string; +} + +export interface OrgAgent { + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + orgDid: string; + verkey: string; + agentEndPoint: string; + agentId: string; + isDidPublic: boolean; + ledgerId: string; + orgAgentTypeId: string; + tenantId: string; +} + +export interface IX509ListCount { + total: number; + data: X509CertificateRecord[]; +} + +export interface IX509CollisionResult { + hasCollision: boolean; + collisions: X509CertificateRecord[]; +} diff --git a/apps/x509/src/main.ts b/apps/x509/src/main.ts new file mode 100644 index 000000000..b5a6d01e4 --- /dev/null +++ b/apps/x509/src/main.ts @@ -0,0 +1,21 @@ +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 { X509Module } from './x509.module'; + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice(X509Module, { + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.X509_NKEY_SEED) + }); + app.useLogger(app.get(NestjsLoggerServiceAdapter)); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + Logger.log('X509 Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/x509/src/repositories/x509.repository.ts b/apps/x509/src/repositories/x509.repository.ts new file mode 100644 index 000000000..037c2d4f0 --- /dev/null +++ b/apps/x509/src/repositories/x509.repository.ts @@ -0,0 +1,326 @@ +/* eslint-disable camelcase */ +// src/repositories/x509-certificate.repository.ts +import { Injectable, NotFoundException, Logger } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +import { + CertificateDateCheckDto, + CreateX509CertificateEntity, + IX509CollisionResult, + IX509ListCount, + OrgAgent, + UpdateCertificateStatusDto +} from '../interfaces/x509.interface'; +import { x5cRecordStatus } from '@credebl/enum/enum'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { org_agents } from '@prisma/client'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; + +@Injectable() +export class X509CertificateRepository { + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} + + // Helper method to get orgAgent by orgId + private async getOrgAgentByOrgId(orgId: string): Promise { + try { + const orgAgent = await this.prisma.org_agents.findFirst({ + where: { + orgId + } + }); + + if (!orgAgent) { + throw new NotFoundException(`OrgAgent with orgId ${orgId} not found`); + } + + return orgAgent; + } catch (error) { + this.logger.error(`Error in getOrgAgentByOrgId: ${error.message}`); + throw error; + } + } + + // CREATE - Create new certificate using orgId + async create(createDto: CreateX509CertificateEntity): Promise { + try { + // Get orgAgent by orgId + const orgAgent = await this.getOrgAgentByOrgId(createDto.orgId); + + const certificate = await this.prisma.x509_certificates.create({ + data: { + orgAgentId: orgAgent.id, + keyType: createDto.keyType, + status: createDto.status, + validFrom: createDto.validFrom, + expiry: createDto.expiry, + certificateBase64: createDto.certificateBase64, + createdBy: createDto.createdBy, + lastChangedBy: createDto.lastChangedBy + } + }); + + return certificate; + } catch (error) { + this.logger.error(`Error in create certificate: ${error.message}`); + throw error; + } + } + + // READ - Find all certificates with optional filtering and pagination + async findAll(options?: { + orgId: string; + status?: string; + keyType?: string; + page?: number; + limit?: number; + }): Promise { + try { + const { orgId, status, keyType, page = 1, limit = 10 } = options || {}; + + const skip = (page - 1) * limit; + + const where: Parameters[0]['where'] = {}; + + // Build where conditions with joins + if (orgId || status || keyType) { + where.AND = []; + + where.AND.push({ + org_agents: { + orgId + } + }); + + if (status) { + where.AND.push({ status }); + } + + if (keyType) { + where.AND.push({ keyType }); + } + } + + const [data, total] = await Promise.all([ + this.prisma.x509_certificates.findMany({ + where, + skip, + take: limit, + orderBy: { + createdAt: 'desc' + } + }), + this.prisma.x509_certificates.count({ where }) + ]); + + return { data, total }; + } catch (error) { + this.logger.error(`Error in findAll certificates: ${error.message}`); + throw error; + } + } + + // READ - Find certificate by ID + async findById(orgId: string, id: string): Promise { + try { + const certificate = await this.prisma.x509_certificates.findUnique({ + where: { + id, + org_agents: { + orgId + } + } + }); + + if (!certificate) { + throw new NotFoundException(`Certificate with ID ${id} not found`); + } + + return certificate; + } catch (error) { + this.logger.error(`Error in findById: ${error.message}`); + throw error; + } + } + + // READ - Find certificates by organization ID + async findByOrgId(orgId: string): Promise { + try { + const certificates = await this.prisma.x509_certificates.findMany({ + where: { + org_agents: { + orgId + } + }, + orderBy: { + createdAt: 'desc' + } + }); + + return certificates; + } catch (error) { + this.logger.error(`Error in findByOrgId: ${error.message}`); + throw error; + } + } + + // UTILITY - Check date collision without throwing exception + async hasDateCollision(dateCheckDto: CertificateDateCheckDto): Promise { + try { + const { orgId, validFrom, expiry, excludeCertificateId } = dateCheckDto; + + const collisions = await this.prisma.x509_certificates.findMany({ + where: { + status: dateCheckDto.status, + keyType: dateCheckDto.keyType, + org_agents: { + orgId + }, + AND: [ + { + OR: [ + { + AND: [{ validFrom: { lte: expiry } }, { expiry: { gte: validFrom } }] + } + ] + }, + ...(excludeCertificateId ? [{ id: { not: excludeCertificateId } }] : []) + ] + } + }); + + return { + hasCollision: 0 < collisions.length, + collisions + }; + } catch (error) { + this.logger.error(`Error in hasDateCollision: ${error.message}`); + throw error; + } + } + + // UPDATE - Update certificate status + async updateStatus(id: string, statusDto: UpdateCertificateStatusDto): Promise { + try { + const certificate = await this.prisma.x509_certificates.update({ + where: { id }, + data: { + status: statusDto.status, + lastChangedBy: statusDto.lastChangedBy + } + }); + + return certificate; + } catch (error) { + this.logger.error(`Error in updateStatus: ${error.message}`); + throw error; + } + } + + // // DELETE - Delete certificate + // async delete(id: string) { + // try { + // await this.findById(id); // Check if certificate exists + + // await this.prisma.x509_certificates.delete({ + // where: { id } + // }); + // } catch (error) { + // this.logger.error(`Error in delete certificate: ${error.message}`); + // throw error; + // } + // } + + // UTILITY - Check if certificate exists + async exists(id: string): Promise { + try { + const count = await this.prisma.x509_certificates.count({ + where: { id } + }); + return 0 < count; + } catch (error) { + this.logger.error(`Error in exists check: ${error.message}`); + throw error; + } + } + + // UTILITY - Find expiring certificates for an org + async findExpiringCertificatesByOrg(orgId: string, days: number = 30): Promise { + try { + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() + days); + + const certificates = await this.prisma.x509_certificates.findMany({ + where: { + org_agents: { + orgId + }, + expiry: { + lte: expiryDate + }, + status: 'Active' + }, + orderBy: { + expiry: 'asc' + } + }); + + return certificates; + } catch (error) { + this.logger.error(`Error in findExpiringCertificatesByOrg: ${error.message}`); + throw error; + } + } + + async getCurrentActiveCertificate(orgId: string): Promise { + try { + const now = new Date(); + + const certificate = await this.prisma.x509_certificates.findFirst({ + where: { + org_agents: { + orgId + }, + status: x5cRecordStatus.Active, + validFrom: { + lte: now + }, + expiry: { + gte: now + } + }, + orderBy: { + createdAt: 'desc' + } + }); + + return certificate; + } catch (error) { + this.logger.error(`Error in getCurrentActiveCertificate: ${error.message}`); + throw error; + } + } + + async getAgentEndPoint(orgId: string): Promise { + try { + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { + orgId + }, + include: { + organisation: true + } + }); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.x509.error.agentEndPointNotFound); + } + + return agentDetails; + } catch (error) { + this.logger.error(`Error in get getAgentEndPoint: ${error.message} `); + throw error; + } + } +} diff --git a/apps/x509/src/x509.controller.ts b/apps/x509/src/x509.controller.ts new file mode 100644 index 000000000..8227e88e4 --- /dev/null +++ b/apps/x509/src/x509.controller.ts @@ -0,0 +1,58 @@ +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { X509CertificateService } from './x509.service'; +import { user } from '@prisma/client'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { + IX509ImportCertificateOptionsDto, + IX509SearchCriteria, + X509CertificateRecord, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; + +@Controller() +export class X509CertificateController { + constructor(private readonly x509CertificateService: X509CertificateService) {} + + @MessagePattern({ cmd: 'create-x509-certificate' }) + async createCertificate(payload: { + orgId: string; + options: X509CreateCertificateOptions; + user: user; + }): Promise { + return this.x509CertificateService.createCertificate(payload); + } + + @MessagePattern({ cmd: 'activate-x509-certificate' }) + async activateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + return this.x509CertificateService.activateCertificate(payload); + } + + @MessagePattern({ cmd: 'deActivate-x509-certificate' }) + async deActivateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + return this.x509CertificateService.deActivateCertificate(payload); + } + + @MessagePattern({ cmd: 'get-all-certificates' }) + async getCertificateByOrgId(payload: { + orgId: string; + options: IX509SearchCriteria; + user: IUserRequest; + }): Promise<{ data: X509CertificateRecord[]; total: number }> { + return this.x509CertificateService.getCertificateByOrgId(payload.orgId, payload.options); + } + + @MessagePattern({ cmd: 'get-certificate' }) + async getCertificate(payload: { orgId: string; id: string; user: IUserRequest }): Promise { + return this.x509CertificateService.getCertificateById(payload.orgId, payload.id); + } + + @MessagePattern({ cmd: 'import-x509-certificate' }) + async importCertificate(payload: { + orgId: string; + options: IX509ImportCertificateOptionsDto; + user: user; + }): Promise { + return this.x509CertificateService.importCertificate(payload); + } +} diff --git a/apps/x509/src/x509.module.ts b/apps/x509/src/x509.module.ts new file mode 100644 index 000000000..8b2a9f0bb --- /dev/null +++ b/apps/x509/src/x509.module.ts @@ -0,0 +1,31 @@ +import { Logger, Module } from '@nestjs/common'; +import { X509CertificateService } from './x509.service'; +import { PrismaService } from '@credebl/prisma-service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { X509CertificateRepository } from './repositories/x509.repository'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; +import { GlobalConfigModule } from '@credebl/config/global-config.module'; +import { LoggerModule } from '@credebl/logger/logger.module'; +import { ContextInterceptorModule } from '@credebl/context/contextInterceptorModule'; +import { X509CertificateController } from './x509.controller'; + +@Module({ + imports: [ + GlobalConfigModule, + LoggerModule, + PlatformConfig, + ContextInterceptorModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.X509_NKEY_SEED) + } + ]) + ], + controllers: [X509CertificateController], + providers: [X509CertificateService, PrismaService, X509CertificateRepository, Logger] +}) +export class X509Module {} diff --git a/apps/x509/src/x509.service.ts b/apps/x509/src/x509.service.ts new file mode 100644 index 000000000..c9c92bdfc --- /dev/null +++ b/apps/x509/src/x509.service.ts @@ -0,0 +1,346 @@ +// src/services/x509-certificate.service.ts +import { + ConflictException, + HttpException, + Inject, + Injectable, + InternalServerErrorException, + NotFoundException, + UnprocessableEntityException +} from '@nestjs/common'; +import { BaseService } from 'libs/service/base.service'; +import { X509CertificateRepository } from './repositories/x509.repository'; +import { user } from '@prisma/client'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { map } from 'rxjs'; +import { + IX509ImportCertificateOptionsDto, + IX509SearchCriteria, + x509CertificateDecodeDto, + X509CertificateRecord, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; +import { + CertificateDateCheckDto, + CreateX509CertificateEntity, + UpdateCertificateStatusDto +} from './interfaces/x509.interface'; +import { getAgentUrl } from '@credebl/common/common.utils'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; + +@Injectable() +export class X509CertificateService extends BaseService { + constructor( + private readonly x509CertificateRepository: X509CertificateRepository, + @Inject('NATS_CLIENT') private readonly x509ServiceProxy: ClientProxy + ) { + super('x509Service'); + } + + async createCertificate(payload: { + orgId: string; + options: X509CreateCertificateOptions; + user: user; + }): Promise { + try { + this.logger.log(`Start creating x509 certificate`); + this.logger.debug(`Create x509 certificate with options`, payload); + const { options, user, orgId } = payload; + const url = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.X509_CREATE_CERTIFICATE); + + const certificateDateCheckDto: CertificateDateCheckDto = { + orgId, + validFrom: options.validity.notBefore, + expiry: options.validity.notAfter, + keyType: options.authorityKey.keyType, + status: x5cRecordStatus.Active + }; + const collisionForActiveRecords = await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + let certStatus: x5cRecordStatus; + if (collisionForActiveRecords.hasCollision) { + certificateDateCheckDto.status = x5cRecordStatus.PendingActivation; + const collisionForPendingRecords = + await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + if (collisionForPendingRecords.hasCollision) { + this.logger.log(`Creating x509 certificate has collision`); + this.logger.error(`Collision records`, collisionForActiveRecords); + throw new ConflictException(ResponseMessages.x509.error.collision); + } + + certStatus = x5cRecordStatus.PendingActivation; + } else { + certStatus = x5cRecordStatus.Active; + } + + const certificate = await this._createX509CertificateForOrg(options, url, orgId); + if (!certificate) { + throw new NotFoundException(ResponseMessages.x509.error.errorCreate); + } + + const createDto: CreateX509CertificateEntity = { + orgId, + certificateBase64: certificate.response.publicCertificateBase64, + keyType: options.authorityKey.keyType, + status: certStatus, + validFrom: options.validity.notBefore, + expiry: options.validity.notAfter, + createdBy: user.id, + lastChangedBy: user.id + }; + + return await this.x509CertificateRepository.create(createDto); + } catch (error) { + this.logger.error(`Error in createCertificate: ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async activateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + const { orgId, user, id } = payload; + const certificateRecord = await this.x509CertificateRepository.findById(orgId, id); + if (certificateRecord) { + const certificateDateCheckDto: CertificateDateCheckDto = { + orgId, + validFrom: certificateRecord.validFrom, + expiry: certificateRecord.expiry, + keyType: certificateRecord.keyType as x5cKeyType, + status: x5cRecordStatus.Active, + excludeCertificateId: id + }; + const collisionForActiveRecords = await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + if (collisionForActiveRecords.hasCollision) { + throw new ConflictException( + `${ResponseMessages.x509.error.collisionForActivatingX5c}. Conflict Records:[${collisionForActiveRecords.collisions.map((collision) => collision.id)}]` + ); + } + const statusDto: UpdateCertificateStatusDto = { + status: x5cRecordStatus.Active, + lastChangedBy: user.id + }; + + return this.x509CertificateRepository.updateStatus(id, statusDto); + } + + throw new NotFoundException(ResponseMessages.x509.error.notFound); + } + + async deActivateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + const { orgId, user, id } = payload; + const certificateRecord = await this.x509CertificateRepository.findById(orgId, id); + if (certificateRecord) { + const statusDto: UpdateCertificateStatusDto = { + status: x5cRecordStatus.InActive, + lastChangedBy: user.id + }; + + return this.x509CertificateRepository.updateStatus(id, statusDto); + } + throw new NotFoundException(ResponseMessages.x509.error.notFound); + } + + async importCertificate(payload: { + orgId: string; + options: IX509ImportCertificateOptionsDto; + user: user; + }): Promise { + try { + const { options, user, orgId } = payload; + const url = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.X509_DECODE_CERTIFICATE); + + this.logger.log(`Decoding certificate to import`); + const decodedResult = await this._decodeX509CertificateForOrg({ certificate: options.certificate }, url, orgId); + if (!decodedResult || !decodedResult.response) { + this.logger.error(`Failed to decode certificate`); + throw new NotFoundException(ResponseMessages.x509.error.errorDecode); + } + + this.logger.log(`Decoded certificate`); + this.logger.debug(`certificate data:`, JSON.stringify(decodedResult)); + + const { publicKey } = decodedResult.response; + const decodedCert = decodedResult.response.x509Certificate; + + this.logger.log(`Start validating certificate`); + const isValidKeyType = Object.values(x5cKeyType).includes(publicKey.keyType as x5cKeyType); + + if (!isValidKeyType) { + this.logger.error(`keyType is not valid for importing certificate`); + throw new InternalServerErrorException(ResponseMessages.x509.error.import); + } + + const validFrom = new Date(decodedCert.notBefore); + const expiry = new Date(decodedCert.notAfter); + const certificateDateCheckDto: CertificateDateCheckDto = { + orgId, + validFrom, + expiry, + keyType: publicKey.keyType, + status: x5cRecordStatus.Active + }; + const collisionForActiveRecords = await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + let certStatus: x5cRecordStatus; + if (collisionForActiveRecords.hasCollision) { + certificateDateCheckDto.status = x5cRecordStatus.PendingActivation; + const collisionForPendingRecords = + await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + if (collisionForPendingRecords.hasCollision) { + this.logger.log(`Importing x509 certificate has collision`); + this.logger.error(`Collision records`, collisionForPendingRecords); + throw new UnprocessableEntityException(ResponseMessages.x509.error.collision); + } + certStatus = x5cRecordStatus.PendingActivation; + } else { + certStatus = x5cRecordStatus.Active; + } + const importurl = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.X509_IMPORT_CERTIFICATE); + + this.logger.log(`Certificate validation done`); + const certificate = await this._importX509CertificateForOrg(options, importurl, orgId); + if (!certificate) { + throw new NotFoundException(ResponseMessages.x509.error.errorCreate); + } + this.logger.log(`Successfully imported certificate in wallet `); + const createDto: CreateX509CertificateEntity = { + orgId, + certificateBase64: certificate.response.issuerCertficicate, + keyType: publicKey.keyType, + status: certStatus, + validFrom, + expiry, + createdBy: user.id, + lastChangedBy: user.id + }; + this.logger.log(`Now adding certificate in platform for org : ${orgId} `); + + return await this.x509CertificateRepository.create(createDto); + } catch (error) { + this.logger.error(`Error in importing certificate: ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async getCertificateByOrgId( + orgId: string, + options: IX509SearchCriteria + ): Promise<{ data: X509CertificateRecord[]; total: number }> { + return this.x509CertificateRepository.findAll({ + orgId, + keyType: options.keyType, + status: options.status, + limit: options.pageSize, + page: options.pageNumber + }); + } + + async getCertificateById(orgId: string, id: string): Promise { + return this.x509CertificateRepository.findById(orgId, id); + } + + async _createX509CertificateForOrg( + options: X509CreateCertificateOptions, + url: string, + orgId: string + ): Promise<{ + response; + }> { + try { + const pattern = { cmd: 'agent-create-x509-certificate' }; + const payload = { options, url, orgId }; + this.logger.log(`Requesing agent service for create x509 certificate`); + this.logger.debug(`agent service payload - _createX509CertificateForOrg : `, payload); + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_createX509CertificateForOrg] [NATS call]- : ${JSON.stringify(error)}`); + throw error; + } + } + + async _decodeX509CertificateForOrg( + options: x509CertificateDecodeDto, + url: string, + orgId: string + ): Promise<{ + response; + }> { + try { + const pattern = { cmd: 'agent-decode-x509-certificate' }; + const payload = { options, url, orgId }; + this.logger.log(`Requesing agent service for decode x509 certificate`); + this.logger.debug(`agent service payload - _decodeX509CertificateForOrg : `, payload); + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_decodeX509CertificateForOrg] [NATS call]- : ${JSON.stringify(error)}`); + throw error; + } + } + + async _importX509CertificateForOrg( + options: IX509ImportCertificateOptionsDto, + url: string, + orgId: string + ): Promise<{ + response; + }> { + try { + const pattern = { cmd: 'agent-import-x509-certificate' }; + const payload = { options, url, orgId }; + this.logger.log(`Requesing agent service for importing x509 certificate`); + this.logger.debug(`agent service payload - _importX509CertificateForOrg : `, payload); + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_importX509CertificateForOrg] [NATS call]- : ${JSON.stringify(error)}`); + throw error; + } + } + + async natsCall( + pattern: object, + payload: object + ): Promise<{ + response: string; + }> { + try { + return this.x509ServiceProxy + .send(pattern, payload) + .pipe( + map((response) => ({ + response + })) + ) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.statusCode, + error: error.message + }, + error.error + ); + }); + } catch (error) { + this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); + throw error; + } + } + + async getAgentEndpoint(orgId: string): Promise { + const agentDetails = await this.x509CertificateRepository.getAgentEndPoint(orgId); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + if (!agentDetails.agentEndPoint || '' === agentDetails.agentEndPoint.trim()) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + return agentDetails.agentEndPoint; + } +} diff --git a/apps/x509/tsconfig.app.json b/apps/x509/tsconfig.app.json new file mode 100644 index 000000000..812e0f82c --- /dev/null +++ b/apps/x509/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/x509" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index ccd3a1751..4254ad114 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -126,6 +126,11 @@ export enum CommonConstants { URL_OIDC_ISSUER_SESSIONS_GET = '/openid4vc/issuance-sessions/#', URL_OIDC_ISSUER_SESSIONS_GET_ALL = '/openid4vc/issuance-sessions', + //X509 agent API URLs + URL_CREATE_X509_CERTIFICATE = '/x509', + URL_IMPORT_X509_CERTIFICATE = '/x509/import', + URL_DECODE_X509_CERTIFICATE = '/x509/decode', + // Nested attribute separator NESTED_ATTRIBUTE_SEPARATOR = '~', @@ -374,6 +379,7 @@ export enum CommonConstants { GEO_LOCATION_SERVICE = 'geo-location', CLOUD_WALLET_SERVICE = 'cloud-wallet', OIDC4VC_ISSUANCE_SERVICE = 'oid4vc-issuance', + X509_SERVICE = 'x509-service', ACCEPT_OFFER = '/didcomm/credentials/accept-offer', SEED_LENGTH = 32, @@ -405,7 +411,12 @@ export enum CommonConstants { OIDC_ISSUER_SESSIONS_UPDATE_OFFER = 'update-oid4vc-credential-offer', OIDC_ISSUER_SESSIONS_BY_ID = 'get-oid4vc-session-by-id', OIDC_ISSUER_SESSIONS = 'get-oid4vc-sessions', - OIDC_DELETE_CREDENTIAL_OFFER = 'delete-oid4vc-credential-offer' + OIDC_DELETE_CREDENTIAL_OFFER = 'delete-oid4vc-credential-offer', + + //X509 + X509_CREATE_CERTIFICATE = 'create-x509-certificate', + X509_IMPORT_CERTIFICATE = 'import-x509-certificate', + X509_DECODE_CERTIFICATE = 'decode-x509-certificate' } export const MICRO_SERVICE_NAME = Symbol('MICRO_SERVICE_NAME'); export const ATTRIBUTE_NAME_REGEX = /\['(.*?)'\]/; diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index 5091239fc..a41b9b72d 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -71,7 +71,6 @@ export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramI if (!agentEndPoint) { throw new NotFoundException(ResponseMessages.common.error.invalidEndpoint); } - const agentUrlMap: Map = new Map([ [String(CommonConstants.CONNECTION_INVITATION), String(CommonConstants.URL_CONN_INVITE)], [String(CommonConstants.LEGACY_INVITATION), String(CommonConstants.URL_CONN_LEGACY_INVITE)], @@ -106,7 +105,10 @@ export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramI [String(CommonConstants.OIDC_ISSUER_SESSIONS_UPDATE_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET)], [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_DELETE_CREDENTIAL_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], + [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)] ]); const urlSuffix = agentUrlMap.get(urlFlag); diff --git a/libs/common/src/interfaces/x509.interface.ts b/libs/common/src/interfaces/x509.interface.ts new file mode 100644 index 000000000..17c350c22 --- /dev/null +++ b/libs/common/src/interfaces/x509.interface.ts @@ -0,0 +1,246 @@ +import { X509ExtendedKeyUsage, X509KeyUsage, x5cKeyType } from '@credebl/enum/enum'; + +// Enum remains the same +export enum GeneralNameType { + DNS = 'dns', + DN = 'dn', + EMAIL = 'email', + GUID = 'guid', + IP = 'ip', + URL = 'url', + UPN = 'upn', + REGISTERED_ID = 'id' +} + +export interface AuthorityAndSubjectKey { + // /** + // * @example "my-seed-12345" + // * @description Seed to deterministically derive the key (optional) + // */ + // seed?: string; + + // /** + // * @example "3yPQbnk6WwLgX8K3JZ4t7vBnJ8XqY2mMpRcD9fNvGtHw" + // * @description publicKeyBase58 for using existing key in wallet (optional) + // */ + // publicKeyBase58?: string; + + /** + * @example "p256" + * @description Type of the key used for signing the X.509 Certificate (default is p256) + */ + keyType?: x5cKeyType; +} + +export interface Name { + /** + * @example "dns" + */ + type: GeneralNameType; + + /** + * @example "example.com" + */ + value: string; +} + +export interface X509CertificateIssuerAndSubjectOptions { + /** + * @example "US" + */ + countryName?: string; + + /** + * @example "California" + */ + stateOrProvinceName?: string; + + /** + * @example "IT Department" + */ + organizationalUnit?: string; + + /** + * @example "Example Corporation" + */ + commonName?: string; +} + +export interface Validity { + /** + * @example "2024-01-01T00:00:00.000Z" + */ + notBefore?: Date; + + /** + * @example "2025-01-01T00:00:00.000Z" + */ + notAfter?: Date; +} + +export interface KeyUsage { + /** + * @example ["digitalSignature", "keyEncipherment", "crlSign"] + */ + usages: X509KeyUsage[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface ExtendedKeyUsage { + /** + * @example ["MdlDs", "ServerAuth", "ClientAuth"] + */ + usages: X509ExtendedKeyUsage[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface NameList { + /** + * @example [{ "type": "dns", "value": "example.com" }, { "type": "email", "value": "admin@example.com" }] + */ + name: Name[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface AuthorityAndSubjectKeyIdentifier { + /** + * @example true + */ + include: boolean; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface BasicConstraints { + /** + * @example false + */ + ca: boolean; + + /** + * @example 0 + */ + pathLenConstraint?: number; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface CrlDistributionPoints { + /** + * @example ["http://crl.example.com/ca.crl"] + */ + urls: string[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface X509CertificateExtensionsOptions { + keyUsage?: KeyUsage; + extendedKeyUsage?: ExtendedKeyUsage; + authorityKeyIdentifier?: AuthorityAndSubjectKeyIdentifier; + subjectKeyIdentifier?: AuthorityAndSubjectKeyIdentifier; + issuerAlternativeName?: NameList; + subjectAlternativeName?: NameList; + basicConstraints?: BasicConstraints; + crlDistributionPoints?: CrlDistributionPoints; +} + +export interface X509CreateCertificateOptions { + authorityKey?: AuthorityAndSubjectKey; + subjectPublicKey?: AuthorityAndSubjectKey; + + /** + * @example "1234567890" + */ + serialNumber?: string; + + /** + * @example { + * "countryName": "US", + * "stateOrProvinceName": "California", + * "commonName": "Example CA" + * } + * OR + * @example "/C=US/ST=California/O=Example Corporation/CN=Example CA" + */ + issuer: X509CertificateIssuerAndSubjectOptions | string; + + /** + * @example { + * "countryName": "US", + * "commonName": "www.example.com" + * } + * OR + * @example "/C=US/CN=www.example.com" + */ + subject?: X509CertificateIssuerAndSubjectOptions | string; + + validity?: Validity; + extensions?: X509CertificateExtensionsOptions; +} + +export interface X509CertificateRecord { + id: string; + orgAgentId: string; + keyType: string; + status: string; + validFrom: Date; + expiry: Date; + certificateBase64: string; + createdBy: string; + lastChangedBy: string; + createdAt: Date; + lastChangedDateTime: Date; +} + +export interface IX509SearchCriteria extends IPaginationSortingdto { + keyType: string; + status: string; +} + +export interface IPaginationSortingdto { + pageNumber: number; + pageSize: number; + sortField?: string; + sortBy?: string; + searchByText?: string; +} + +export interface IX509ImportCertificateOptionsDto { + /* + X.509 certificate in base64 string format + */ + certificate: string; + + /* + Private key in base64 string format + */ + privateKey?: string; + + keyType: KeyType; +} + +export interface x509CertificateDecodeDto { + certificate: string; +} diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index f76c899eb..71a08808b 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -554,6 +554,28 @@ export const ResponseMessages = { deleteFailed: 'Failed to delete OID4VC credential offer.' } }, + x509: { + success: { + create: 'x509 certificate created successfully', + activated: 'x509 certificate activated successfully', + deActivated: 'x509 certificate deactivated successfully', + fetch: 'x509 certificate fetched successfully', + fetchAll: 'x509 certificates fetched successfully', + import: 'x509 certificate imported successfully' + }, + error: { + errorCreate: 'Error while creating x509 certificate.', + errorUpdateStatus: 'Error while updating x509 certificate.', + errorActivation: 'Failed to activate x509 certificate..', + agentEndPointNotFound: 'Agent details not found', + collision: 'Certificate date range collides with existing certificates for this organization', + collisionForActivatingX5c: + 'Certificate date range collides with existing certificates for this organization, In order to active this you need to Inactivate the previous one.', + notFound: 'x509 certificate record not found.', + import: 'Failed to import x509 certificate', + errorDecode: 'Error while decoding x509 certificate.' + } + }, nats: { success: {}, error: { diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 787fc1f92..0a52130df 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -272,7 +272,7 @@ export enum ProviderType { SUPABASE = 'supabase' } -export declare enum OpenId4VcIssuanceSessionState { +export enum OpenId4VcIssuanceSessionState { OfferCreated = 'OfferCreated', OfferUriRetrieved = 'OfferUriRetrieved', AuthorizationInitiated = 'AuthorizationInitiated', @@ -284,3 +284,41 @@ export declare enum OpenId4VcIssuanceSessionState { Completed = 'Completed', Error = 'Error' } + +export enum x5cKeyType { + Ed25519 = 'ed25519', + P256 = 'p256' +} + +export enum x5cRecordStatus { + Active = 'Active', + PendingActivation = 'Pending activation', + InActive = 'In Active' +} + +export enum X509KeyUsage { + DigitalSignature = 1, + NonRepudiation = 2, + KeyEncipherment = 4, + DataEncipherment = 8, + KeyAgreement = 16, + KeyCertSign = 32, + CrlSign = 64, + EncipherOnly = 128, + DecipherOnly = 256 +} + +export enum X509ExtendedKeyUsage { + ServerAuth = '1.3.6.1.5.5.7.3.1', + ClientAuth = '1.3.6.1.5.5.7.3.2', + CodeSigning = '1.3.6.1.5.5.7.3.3', + EmailProtection = '1.3.6.1.5.5.7.3.4', + TimeStamping = '1.3.6.1.5.5.7.3.8', + OcspSigning = '1.3.6.1.5.5.7.3.9', + MdlDs = '1.0.18013.5.1.2' +} + +export enum CredentialFormat { + SdJwtVc = 'vc+sd-jwt', + Mdoc = 'mso_mdoc' +} diff --git a/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql b/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql new file mode 100644 index 000000000..21ce60a0f --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql @@ -0,0 +1,58 @@ +/* + Warnings: + + - You are about to drop the column `supported_protocol` on the `ledgers` table. All the data in the column will be lost. + - You are about to drop the column `supported_protocol` on the `organisation` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "ledgers" DROP COLUMN "supported_protocol"; + +-- AlterTable +ALTER TABLE "organisation" DROP COLUMN "supported_protocol"; + +-- DropEnum +DROP TYPE "CredentialExchangeProtocol"; + +-- CreateTable +CREATE TABLE "oid4vc_credentials" ( + "id" UUID NOT NULL, + "orgId" UUID NOT NULL, + "offerId" TEXT NOT NULL, + "credentialOfferId" TEXT NOT NULL, + "state" TEXT NOT NULL, + "contextCorrelationId" TEXT NOT NULL, + "createdBy" UUID NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" UUID NOT NULL, + + CONSTRAINT "oid4vc_credentials_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "x509_certificates" ( + "id" TEXT NOT NULL, + "orgAgentId" UUID NOT NULL, + "keyType" TEXT NOT NULL, + "status" TEXT NOT NULL, + "validFrom" TIMESTAMP(3) NOT NULL, + "expiry" TIMESTAMP(3) NOT NULL, + "certificateBase64" TEXT NOT NULL, + "isImported" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" UUID NOT NULL, + "lastChangedDateTime" TIMESTAMP(3) NOT NULL, + "lastChangedBy" UUID NOT NULL, + + CONSTRAINT "x509_certificates_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "oid4vc_credentials_offerId_key" ON "oid4vc_credentials"("offerId"); + +-- AddForeignKey +ALTER TABLE "oid4vc_credentials" ADD CONSTRAINT "oid4vc_credentials_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organisation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "x509_certificates" ADD CONSTRAINT "x509_certificates_orgAgentId_fkey" FOREIGN KEY ("orgAgentId") REFERENCES "org_agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 54f3d7840..495acd350 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -231,6 +231,7 @@ model org_agents { webhookUrl String? @db.VarChar org_dids org_dids[] oidc_issuer oidc_issuer[] + x509_certificates x509_certificates[] } model org_dids { @@ -622,3 +623,24 @@ model credential_templates { signerOption SignerOption } +model x509_certificates { + id String @id @default(uuid()) + + orgAgentId String @db.Uuid + org_agents org_agents @relation(fields: [orgAgentId], references: [id]) + + keyType String // "p256", "ed25519" + status String //e.g "Active", "Pending activation", "InActive" + validFrom DateTime + + expiry DateTime + certificateBase64 String + isImported Boolean @default(false) + + createdAt DateTime @default(now()) + createdBy String @db.Uuid + lastChangedDateTime DateTime @updatedAt + lastChangedBy String @db.Uuid +} + + diff --git a/nest-cli.json b/nest-cli.json index 819afa8b7..82ff28c5d 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -295,6 +295,15 @@ "compilerOptions": { "tsConfigPath": "apps/oid4vc-issuance/tsconfig.app.json" } + }, + "x509": { + "type": "application", + "root": "apps/x509", + "entryFile": "main", + "sourceRoot": "apps/x509/src", + "compilerOptions": { + "tsConfigPath": "apps/x509/tsconfig.app.json" + } } } } \ No newline at end of file From 967ead58471f2cd1b47cd95ebc135818d3d6dcfb Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Mon, 13 Oct 2025 16:13:07 +0530 Subject: [PATCH 06/43] fixing eslint issues Signed-off-by: Rinkal Bhojani --- apps/agent-service/test/app.e2e-spec.ts | 7 +- .../cloud-wallet/enums/connections.enum.ts | 6 +- .../src/connection/enums/connections.enum.ts | 6 +- .../src/connection/interfaces/index.ts | 10 +- .../src/dtos/credential-offer.dto.ts | 57 ++++--- .../src/dtos/credential-send-offer.dto.ts | 13 +- .../src/helper-files/file-operation.helper.ts | 52 +++--- .../src/verification/dto/webhook-proof.dto.ts | 153 +++++++++--------- .../interfaces/verification.interface.ts | 118 +++++++------- .../src/interfaces/connection.interfaces.ts | 28 ++-- apps/issuance/src/issuance.service.ts | 2 +- apps/user/test/app.e2e-spec.ts | 7 +- apps/utility/src/utilities.repository.ts | 83 +++++----- .../src/nestjsClsContextStorageService.ts | 11 +- libs/logger/src/winstonLogger.ts | 73 ++++----- 15 files changed, 289 insertions(+), 337 deletions(-) diff --git a/apps/agent-service/test/app.e2e-spec.ts b/apps/agent-service/test/app.e2e-spec.ts index 58f95a822..0a642e6bd 100644 --- a/apps/agent-service/test/app.e2e-spec.ts +++ b/apps/agent-service/test/app.e2e-spec.ts @@ -15,10 +15,5 @@ describe('AgentServiceController (e2e)', () => { await app.init(); }); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); + it('/ (GET)', () => request(app.getHttpServer()).get('/').expect(200).expect('Hello World!')); }); diff --git a/apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts b/apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts index a15929cf6..9c76afc13 100644 --- a/apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts +++ b/apps/api-gateway/src/cloud-wallet/enums/connections.enum.ts @@ -1,4 +1,4 @@ export declare enum HandshakeProtocol { - Connections = "https://didcomm.org/connections/1.0", - DidExchange = "https://didcomm.org/didexchange/1.0" -} \ No newline at end of file + Connections = 'https://didcomm.org/connections/1.0', + DidExchange = 'https://didcomm.org/didexchange/1.0' +} diff --git a/apps/api-gateway/src/connection/enums/connections.enum.ts b/apps/api-gateway/src/connection/enums/connections.enum.ts index a15929cf6..9c76afc13 100644 --- a/apps/api-gateway/src/connection/enums/connections.enum.ts +++ b/apps/api-gateway/src/connection/enums/connections.enum.ts @@ -1,4 +1,4 @@ export declare enum HandshakeProtocol { - Connections = "https://didcomm.org/connections/1.0", - DidExchange = "https://didcomm.org/didexchange/1.0" -} \ No newline at end of file + Connections = 'https://didcomm.org/connections/1.0', + DidExchange = 'https://didcomm.org/didexchange/1.0' +} diff --git a/apps/api-gateway/src/connection/interfaces/index.ts b/apps/api-gateway/src/connection/interfaces/index.ts index 20f8f584b..acba5bc15 100644 --- a/apps/api-gateway/src/connection/interfaces/index.ts +++ b/apps/api-gateway/src/connection/interfaces/index.ts @@ -26,8 +26,7 @@ export interface ISelectedOrgInterface { export interface IOrganizationInterface { name: string; description: string; - org_agents: IOrgAgentInterface[] - + org_agents: IOrgAgentInterface[]; } export interface IOrgAgentInterface { @@ -40,7 +39,6 @@ export interface IOrgAgentInterface { orgId: string; } - export class IConnectionInterface { tag: object; createdAt: string; @@ -71,8 +69,8 @@ interface IOutOfBandInvitationService { } interface IOutOfBandInvitation { - "@type": string; - "@id": string; + '@type': string; + '@id': string; label: string; accept: string[]; handshake_protocols: string[]; @@ -114,4 +112,4 @@ interface IConnectionRecord { export interface IReceiveInvitationRes { outOfBandRecord: IOutOfBandRecord; connectionRecord: IConnectionRecord; -} \ No newline at end of file +} diff --git a/apps/api-gateway/src/dtos/credential-offer.dto.ts b/apps/api-gateway/src/dtos/credential-offer.dto.ts index c59e54406..39a4e2b7d 100644 --- a/apps/api-gateway/src/dtos/credential-offer.dto.ts +++ b/apps/api-gateway/src/dtos/credential-offer.dto.ts @@ -1,40 +1,39 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsNotEmpty, IsString } from "class-validator"; +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsNotEmpty, IsString } from 'class-validator'; interface attributeValue { - name: string, - value: string, + name: string; + value: string; } export class IssueCredentialOffer { + @ApiProperty({ example: { protocolVersion: 'v1' } }) + @IsNotEmpty({ message: 'Please provide valid protocol-version' }) + @IsString({ message: 'protocol-version should be string' }) + protocolVersion: string; - @ApiProperty({ example: { 'protocolVersion': 'v1' } }) - @IsNotEmpty({ message: 'Please provide valid protocol-version' }) - @IsString({ message: 'protocol-version should be string' }) - protocolVersion: string; - - @ApiProperty({ example: { 'attributes': [{ 'value': 'string', 'name': 'string' }] } }) - @IsNotEmpty({ message: 'Please provide valid attributes' }) - @IsArray({ message: 'attributes should be array' }) - attributes: attributeValue[]; + @ApiProperty({ example: { attributes: [{ value: 'string', name: 'string' }] } }) + @IsNotEmpty({ message: 'Please provide valid attributes' }) + @IsArray({ message: 'attributes should be array' }) + attributes: attributeValue[]; - @ApiProperty({ example: { 'credentialDefinitionId': 'string' } }) - @IsNotEmpty({ message: 'Please provide valid credentialDefinitionId' }) - @IsString({ message: 'credentialDefinitionId should be string' }) - credentialDefinitionId: string; + @ApiProperty({ example: { credentialDefinitionId: 'string' } }) + @IsNotEmpty({ message: 'Please provide valid credentialDefinitionId' }) + @IsString({ message: 'credentialDefinitionId should be string' }) + credentialDefinitionId: string; - @ApiProperty({ example: { autoAcceptCredential: 'always' } }) - @IsNotEmpty({ message: 'Please provide valid autoAcceptCredential' }) - @IsString({ message: 'autoAcceptCredential should be string' }) - autoAcceptCredential: string; + @ApiProperty({ example: { autoAcceptCredential: 'always' } }) + @IsNotEmpty({ message: 'Please provide valid autoAcceptCredential' }) + @IsString({ message: 'autoAcceptCredential should be string' }) + autoAcceptCredential: string; - @ApiProperty({ example: { comment: 'string' } }) - @IsNotEmpty({ message: 'Please provide valid comment' }) - @IsString({ message: 'comment should be string' }) - comment: string; + @ApiProperty({ example: { comment: 'string' } }) + @IsNotEmpty({ message: 'Please provide valid comment' }) + @IsString({ message: 'comment should be string' }) + comment: string; - @ApiProperty({ example: { connectionId: '3fa85f64-5717-4562-b3fc-2c963f66afa6' } }) - @IsNotEmpty({ message: 'Please provide valid connection-id' }) - @IsString({ message: 'Connection-id should be string' }) - connectionId: string; + @ApiProperty({ example: { connectionId: '3fa85f64-5717-4562-b3fc-2c963f66afa6' } }) + @IsNotEmpty({ message: 'Please provide valid connection-id' }) + @IsString({ message: 'Connection-id should be string' }) + connectionId: string; } diff --git a/apps/api-gateway/src/dtos/credential-send-offer.dto.ts b/apps/api-gateway/src/dtos/credential-send-offer.dto.ts index 1467ef6af..94afa97ee 100644 --- a/apps/api-gateway/src/dtos/credential-send-offer.dto.ts +++ b/apps/api-gateway/src/dtos/credential-send-offer.dto.ts @@ -1,10 +1,9 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsString } from "class-validator"; +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; export class CredentialSendOffer { - - @ApiProperty({ example: 'string' }) - @IsNotEmpty({ message: 'Please provide valid credentialRecordId' }) - @IsString({ message: 'credentialRecordId should be string' }) - credentialRecordId: string; + @ApiProperty({ example: 'string' }) + @IsNotEmpty({ message: 'Please provide valid credentialRecordId' }) + @IsString({ message: 'credentialRecordId should be string' }) + credentialRecordId: string; } diff --git a/apps/api-gateway/src/helper-files/file-operation.helper.ts b/apps/api-gateway/src/helper-files/file-operation.helper.ts index e02d7315b..5227d0604 100644 --- a/apps/api-gateway/src/helper-files/file-operation.helper.ts +++ b/apps/api-gateway/src/helper-files/file-operation.helper.ts @@ -1,36 +1,26 @@ -import { promisify } from "util"; -import * as fs from "fs"; +import { promisify } from 'util'; +import * as fs from 'fs'; +export const createFile = async (path: string, fileName: string, data: string): Promise => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + if (!checkIfFileOrDirectoryExists(path)) { + fs.mkdirSync(path); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const writeFile = promisify(fs.writeFile); + return fs.writeFileSync(`${path}/${fileName}`, data, 'utf8'); +}; -export const createFile = async ( - path: string, - fileName: string, - data: string - ): Promise => { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - if (!checkIfFileOrDirectoryExists(path)) { - - fs.mkdirSync(path); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const writeFile = promisify(fs.writeFile); - return fs.writeFileSync(`${path}/${fileName}`, data, 'utf8'); - }; +export const checkIfFileOrDirectoryExists = (path: string): boolean => fs.existsSync(path); - export const checkIfFileOrDirectoryExists = (path: string): boolean => fs.existsSync(path); - - export const getFile = async ( - path: string, - encoding: BufferEncoding - ): Promise => { - const readFile = promisify(fs.readFile); - - return encoding ? readFile(path, {encoding}) : readFile(path, {}); - }; +export const getFile = async (path: string, encoding: BufferEncoding): Promise => { + const readFile = promisify(fs.readFile); + return encoding ? readFile(path, { encoding }) : readFile(path, {}); +}; - export const deleteFile = async (path: string): Promise => { - const unlink = promisify(fs.unlink); - - return unlink(path); - }; \ No newline at end of file +export const deleteFile = async (path: string): Promise => { + const unlink = promisify(fs.unlink); + + return unlink(path); +}; diff --git a/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts b/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts index d4f150c48..1f71f5c52 100644 --- a/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts +++ b/apps/api-gateway/src/verification/dto/webhook-proof.dto.ts @@ -1,83 +1,82 @@ -import { ApiPropertyOptional } from "@nestjs/swagger"; -import { IsOptional } from "class-validator"; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional } from 'class-validator'; interface IWebhookPresentationProof { - threadId: string; - state: string; - connectionId + threadId: string; + state: string; + connectionId; } export class WebhookPresentationProofDto { + @ApiPropertyOptional() + @IsOptional() + metadata: object; - @ApiPropertyOptional() - @IsOptional() - metadata: object; - - @ApiPropertyOptional() - @IsOptional() - _tags: IWebhookPresentationProof; - - @ApiPropertyOptional() - @IsOptional() - id: string; - - @ApiPropertyOptional() - @IsOptional() - createdAt: string; - - @ApiPropertyOptional() - @IsOptional() - protocolVersion: string; - - @ApiPropertyOptional() - @IsOptional() - state: string; - - @ApiPropertyOptional() - @IsOptional() - connectionId: string; - - @ApiPropertyOptional() - @IsOptional() - threadId: string; - - @ApiPropertyOptional() - @IsOptional() - parentThreadId?: string; - - @ApiPropertyOptional() - @IsOptional() - presentationId: string; - - @ApiPropertyOptional() - @IsOptional() - autoAcceptProof: string; - - @ApiPropertyOptional() - @IsOptional() - updatedAt: string; - - @ApiPropertyOptional() - @IsOptional() - isVerified: boolean; - - @ApiPropertyOptional() - @IsOptional() - contextCorrelationId: string; - - @ApiPropertyOptional() - @IsOptional() - type: string; - - @ApiPropertyOptional() - @IsOptional() - proofData: object; - - @ApiPropertyOptional() - @IsOptional() - orgId: string; - - @ApiPropertyOptional() - @IsOptional() - errorMessage: string; -} \ No newline at end of file + @ApiPropertyOptional() + @IsOptional() + _tags: IWebhookPresentationProof; + + @ApiPropertyOptional() + @IsOptional() + id: string; + + @ApiPropertyOptional() + @IsOptional() + createdAt: string; + + @ApiPropertyOptional() + @IsOptional() + protocolVersion: string; + + @ApiPropertyOptional() + @IsOptional() + state: string; + + @ApiPropertyOptional() + @IsOptional() + connectionId: string; + + @ApiPropertyOptional() + @IsOptional() + threadId: string; + + @ApiPropertyOptional() + @IsOptional() + parentThreadId?: string; + + @ApiPropertyOptional() + @IsOptional() + presentationId: string; + + @ApiPropertyOptional() + @IsOptional() + autoAcceptProof: string; + + @ApiPropertyOptional() + @IsOptional() + updatedAt: string; + + @ApiPropertyOptional() + @IsOptional() + isVerified: boolean; + + @ApiPropertyOptional() + @IsOptional() + contextCorrelationId: string; + + @ApiPropertyOptional() + @IsOptional() + type: string; + + @ApiPropertyOptional() + @IsOptional() + proofData: object; + + @ApiPropertyOptional() + @IsOptional() + orgId: string; + + @ApiPropertyOptional() + @IsOptional() + errorMessage: string; +} diff --git a/apps/api-gateway/src/verification/interfaces/verification.interface.ts b/apps/api-gateway/src/verification/interfaces/verification.interface.ts index cac18fa67..fc082360c 100644 --- a/apps/api-gateway/src/verification/interfaces/verification.interface.ts +++ b/apps/api-gateway/src/verification/interfaces/verification.interface.ts @@ -1,92 +1,92 @@ -import { IUserRequestInterface } from "../../interfaces/IUserRequestInterface"; +import { IUserRequestInterface } from '../../interfaces/IUserRequestInterface'; export interface IProofRequestAttribute { - attributeName: string; - condition?: string; - value?: string; - credDefId: string; - credentialName: string; + attributeName: string; + condition?: string; + value?: string; + credDefId: string; + credentialName: string; } export interface IProofRequestSearchCriteria { - pageNumber: number; - pageSize: number; - sortField: string; - sortBy: string; - search: string; - user?: IUserRequestInterface + pageNumber: number; + pageSize: number; + sortField: string; + sortBy: string; + search: string; + user?: IUserRequestInterface; } export interface IProofRequest { - metadata: object; - id: string; -} + metadata: object; + id: string; +} export interface IProofPresentation { - createdAt: string; - protocolVersion: string; - state: string; - connectionId: string; - threadId: string; - autoAcceptProof: string; - updatedAt: string; - isVerified: boolean; - } + createdAt: string; + protocolVersion: string; + state: string; + connectionId: string; + threadId: string; + autoAcceptProof: string; + updatedAt: string; + isVerified: boolean; +} export interface IPresentation { - _tags: ITags; - metadata: object; - id: string; - createdAt: string; - protocolVersion: string; - state: string; - connectionId: string; - threadId: string; - autoAcceptProof: string; - updatedAt: string; - isVerified: boolean; - } + _tags: ITags; + metadata: object; + id: string; + createdAt: string; + protocolVersion: string; + state: string; + connectionId: string; + threadId: string; + autoAcceptProof: string; + updatedAt: string; + isVerified: boolean; +} interface ITags { - connectionId: string; - state: string; - threadId: string; -} + connectionId: string; + state: string; + threadId: string; +} export interface IProofFormats { - indy: IndyProof + indy: IndyProof; } interface IndyProof { - name: string; - version: string; - requested_attributes: IRequestedAttributes; - requested_predicates: IRequestedPredicates; + name: string; + version: string; + requested_attributes: IRequestedAttributes; + requested_predicates: IRequestedPredicates; } interface IRequestedAttributes { - [key: string]: IRequestedAttributesName; + [key: string]: IRequestedAttributesName; } interface IRequestedAttributesName { - name?: string; - names?: string; - restrictions: IRequestedRestriction[] + name?: string; + names?: string; + restrictions: IRequestedRestriction[]; } interface IRequestedPredicates { - [key: string]: IRequestedPredicatesName; + [key: string]: IRequestedPredicatesName; } interface IRequestedPredicatesName { - name: string; - restrictions: IRequestedRestriction[] + name: string; + restrictions: IRequestedRestriction[]; } interface IRequestedRestriction { - cred_def_id?: string; - schema_id?: string; - schema_issuer_did?: string; - schema_name?: string; - issuer_did?: string; - schema_version?: string; -} \ No newline at end of file + cred_def_id?: string; + schema_id?: string; + schema_issuer_did?: string; + schema_name?: string; + issuer_did?: string; + schema_version?: string; +} diff --git a/apps/connection/src/interfaces/connection.interfaces.ts b/apps/connection/src/interfaces/connection.interfaces.ts index 020e4821d..dad9de317 100644 --- a/apps/connection/src/interfaces/connection.interfaces.ts +++ b/apps/connection/src/interfaces/connection.interfaces.ts @@ -16,7 +16,7 @@ export interface IConnection { handshakeProtocols: string[]; orgId: string; recipientKey?: string; - invitationDid?: string + invitationDid?: string; } export interface IUserRequestInterface { userId: string; @@ -130,7 +130,7 @@ export interface IConnectionSearchCriteria { sortField: string; sortBy: string; searchByText: string; - user: IUserRequestInterface + user: IUserRequestInterface; } export interface AgentConnectionSearchCriteria { @@ -143,9 +143,9 @@ export interface AgentConnectionSearchCriteria { } export interface IReceiveInvitationByUrlOrg { - user: IUserRequestInterface, - receiveInvitationUrl: IReceiveInvitationUrl, - orgId: string + user: IUserRequestInterface; + receiveInvitationUrl: IReceiveInvitationUrl; + orgId: string; } export interface IReceiveInvitationUrl extends IReceiveInvite { @@ -153,9 +153,9 @@ export interface IReceiveInvitationUrl extends IReceiveInvite { } export interface IReceiveInvitationByOrg { - user: IUserRequestInterface, - receiveInvitation: IReceiveInvitation, - orgId: string + user: IUserRequestInterface; + receiveInvitation: IReceiveInvitation; + orgId: string; } interface Service { @@ -210,8 +210,8 @@ interface OutOfBandInvitationService { } interface OutOfBandInvitation { - "@type": string; - "@id": string; + '@type': string; + '@id': string; label: string; accept: string[]; handshake_protocols: string[]; @@ -266,7 +266,7 @@ export interface ConnectionResponseDetail { lastChangedDateTime: Date; lastChangedBy: number; recordId: string; - invitationDid?: string + invitationDid?: string; } export interface ICreateConnectionInvitation { @@ -289,6 +289,6 @@ export interface ICreateConnectionInvitation { } export interface ICreateOutOfbandConnectionInvitation { - user: IUserRequestInterface, - createOutOfBandConnectionInvitation: ICreateConnectionInvitation, -} \ No newline at end of file + user: IUserRequestInterface; + createOutOfBandConnectionInvitation: ICreateConnectionInvitation; +} diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 33b630113..a247c943f 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -1516,7 +1516,7 @@ export class IssuanceService { return schemaDetails; } - async delay(ms): Promise { + async delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/apps/user/test/app.e2e-spec.ts b/apps/user/test/app.e2e-spec.ts index ab51d4a94..ae537da6c 100644 --- a/apps/user/test/app.e2e-spec.ts +++ b/apps/user/test/app.e2e-spec.ts @@ -15,10 +15,5 @@ describe('UserController (e2e)', () => { await app.init(); }); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); - }); + it('/ (GET)', () => request(app.getHttpServer()).get('/').expect(200).expect('Hello World!')); }); diff --git a/apps/utility/src/utilities.repository.ts b/apps/utility/src/utilities.repository.ts index 7727d1b8e..4703af487 100644 --- a/apps/utility/src/utilities.repository.ts +++ b/apps/utility/src/utilities.repository.ts @@ -1,51 +1,46 @@ -import { PrismaService } from "@credebl/prisma-service"; -import { Injectable, Logger } from "@nestjs/common"; +import { PrismaService } from '@credebl/prisma-service'; +import { Injectable, Logger } from '@nestjs/common'; // eslint-disable-next-line camelcase -import { shortening_url } from "@prisma/client"; +import { shortening_url } from '@prisma/client'; @Injectable() export class UtilitiesRepository { - constructor( - private readonly prisma: PrismaService, - private readonly logger: Logger - ) { } - - async saveShorteningUrl( - payload - ): Promise { - - try { - - const { referenceId, invitationPayload } = payload; - const storeShorteningUrl = await this.prisma.shortening_url.upsert({ - where: { referenceId }, - update: { invitationPayload }, - create: { referenceId, invitationPayload } - }); - - this.logger.log(`[saveShorteningUrl] - shortening url details ${referenceId}`); - return storeShorteningUrl; - } catch (error) { - this.logger.error(`Error in saveShorteningUrl: ${error} `); - throw error; - } + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} + + async saveShorteningUrl(payload): Promise { + try { + const { referenceId, invitationPayload } = payload; + const storeShorteningUrl = await this.prisma.shortening_url.upsert({ + where: { referenceId }, + update: { invitationPayload }, + create: { referenceId, invitationPayload } + }); + + this.logger.log(`[saveShorteningUrl] - shortening url details ${referenceId}`); + return storeShorteningUrl; + } catch (error) { + this.logger.error(`Error in saveShorteningUrl: ${error} `); + throw error; } - - // eslint-disable-next-line camelcase - async getShorteningUrl(referenceId): Promise { - try { - - const storeShorteningUrl = await this.prisma.shortening_url.findUnique({ - where: { - referenceId - } - }); - - this.logger.log(`[getShorteningUrl] - shortening url details ${referenceId}`); - return storeShorteningUrl; - } catch (error) { - this.logger.error(`Error in getShorteningUrl: ${error} `); - throw error; + } + + // eslint-disable-next-line camelcase + async getShorteningUrl(referenceId): Promise { + try { + const storeShorteningUrl = await this.prisma.shortening_url.findUnique({ + where: { + referenceId } + }); + + this.logger.log(`[getShorteningUrl] - shortening url details ${referenceId}`); + return storeShorteningUrl; + } catch (error) { + this.logger.error(`Error in getShorteningUrl: ${error} `); + throw error; } -} \ No newline at end of file + } +} diff --git a/libs/context/src/nestjsClsContextStorageService.ts b/libs/context/src/nestjsClsContextStorageService.ts index 00107f21e..7c5593057 100644 --- a/libs/context/src/nestjsClsContextStorageService.ts +++ b/libs/context/src/nestjsClsContextStorageService.ts @@ -3,17 +3,14 @@ import { CLS_ID, ClsService } from 'nestjs-cls'; import { Injectable } from '@nestjs/common'; @Injectable() -export default class NestjsClsContextStorageService - implements ContextStorageService -{ - constructor(private readonly cls: ClsService) { - } +export default class NestjsClsContextStorageService implements ContextStorageService { + constructor(private readonly cls: ClsService) {} public get(key: string): T | undefined { return this.cls.get(key); } - public setContextId(id: string) : void { + public setContextId(id: string): void { this.cls.set(CLS_ID, id); } @@ -24,4 +21,4 @@ export default class NestjsClsContextStorageService public set(key: string, value: T): void { this.cls.set(key, value); } -} \ No newline at end of file +} diff --git a/libs/logger/src/winstonLogger.ts b/libs/logger/src/winstonLogger.ts index d2a8b0ef8..ed8004cd0 100644 --- a/libs/logger/src/winstonLogger.ts +++ b/libs/logger/src/winstonLogger.ts @@ -9,42 +9,34 @@ export const WinstonLoggerTransportsKey = Symbol(); let esTransport; if ('true' === process.env.ELK_LOG?.toLowerCase()) { const esTransportOpts = { - level: `${process.env.LOG_LEVEL}`, - clientOpts: { node: `${process.env.ELK_LOG_PATH}`, - auth: { - username: `${process.env.ELK_USERNAME}`, - password: `${process.env.ELK_PASSWORD}` - } - } -}; -esTransport = new Elasticsearch.ElasticsearchTransport(esTransportOpts); - -esTransport.on('error', (error) => { - console.error('Error caught in logger', error); -}); - + level: `${process.env.LOG_LEVEL}`, + clientOpts: { + node: `${process.env.ELK_LOG_PATH}`, + auth: { + username: `${process.env.ELK_USERNAME}`, + password: `${process.env.ELK_PASSWORD}` + } + } + }; + esTransport = new Elasticsearch.ElasticsearchTransport(esTransportOpts); } - @Injectable() export default class WinstonLogger implements Logger { private readonly logger: winston.Logger; - public constructor( - @Inject(WinstonLoggerTransportsKey) transports: winston.transport[] - ) { - if(esTransport){ + public constructor(@Inject(WinstonLoggerTransportsKey) transports: winston.transport[]) { + if (esTransport) { transports.push(esTransport); } - - + // Create winston logger this.logger = winston.createLogger(this.getLoggerFormatOptions(transports)); - } - private getLoggerFormatOptions(transports: winston.transport[]) : winston.LoggerOptions { + private getLoggerFormatOptions(transports: winston.transport[]): winston.LoggerOptions { // Setting log levels for winston + // eslint-disable-next-line @typescript-eslint/no-explicit-any const levels: any = {}; let cont = 0; Object.values(LogLevel).forEach((level) => { @@ -55,8 +47,8 @@ export default class WinstonLogger implements Logger { return { level: LogLevel.Debug, levels, - // format: ecsFormat.ecsFormat({ convertReqRes: true }), - format: winston.format.combine( + // format: ecsFormat.ecsFormat({ convertReqRes: true }), + format: winston.format.combine( ecsFormat.ecsFormat({ convertReqRes: true }), // Add timestamp and format the date // winston.format.timestamp({ @@ -65,7 +57,8 @@ export default class WinstonLogger implements Logger { // Errors will be logged with stack trace winston.format.errors({ stack: true }), // Add custom Log fields to the log - winston.format((info, opts) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + winston.format((info, _opts) => { // Info contains an Error property if (info.error && info.error instanceof Error) { info.stack = info.error.stack; @@ -82,21 +75,16 @@ export default class WinstonLogger implements Logger { fillExcept: ['timestamp', 'level', 'message'] }), // Format the log as JSON - winston.format.json(), + winston.format.json() ), transports, exceptionHandlers: transports }; } - public log( - level: LogLevel, - message: string | Error, - data?: LogData, - profile?: string - ) : void { + public log(level: LogLevel, message: string | Error, data?: LogData, profile?: string): void { const logData = { - level: level, + level, message: message instanceof Error ? message.message : message, error: message instanceof Error ? message : undefined, ...data @@ -109,34 +97,31 @@ export default class WinstonLogger implements Logger { } } - - public debug(message: string, data?: LogData, profile?: string) : void { + public debug(message: string, data?: LogData, profile?: string): void { this.log(LogLevel.Debug, message, data, profile); } - public info(message: string, data?: LogData, profile?: string) : void { + public info(message: string, data?: LogData, profile?: string): void { this.log(LogLevel.Info, message, data, profile); } - public warn(message: string | Error, data?: LogData, profile?: string) : void { + public warn(message: string | Error, data?: LogData, profile?: string): void { this.log(LogLevel.Warn, message, data, profile); } - public error(message: string | Error, data?: LogData, profile?: string) : void { + public error(message: string | Error, data?: LogData, profile?: string): void { this.log(LogLevel.Error, message, data, profile); } - public fatal(message: string | Error, data?: LogData, profile?: string) : void { + public fatal(message: string | Error, data?: LogData, profile?: string): void { this.log(LogLevel.Fatal, message, data, profile); } - public emergency(message: string | Error, data?: LogData, profile?: string) : void { + public emergency(message: string | Error, data?: LogData, profile?: string): void { this.log(LogLevel.Emergency, message, data, profile); } - public startProfile(id: string) : void { + public startProfile(id: string): void { this.logger.profile(id); } } - - From f946434593a57ba90a2fd55539e359746b4695a6 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Thu, 23 Oct 2025 21:50:02 +0530 Subject: [PATCH 07/43] fix: linting issues Signed-off-by: Rinkal Bhojani --- .../src/helper-files/file-operation.helper.ts | 2 +- .../verification/interfaces/verification.interface.ts | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/apps/api-gateway/src/helper-files/file-operation.helper.ts b/apps/api-gateway/src/helper-files/file-operation.helper.ts index 5227d0604..15ace62be 100644 --- a/apps/api-gateway/src/helper-files/file-operation.helper.ts +++ b/apps/api-gateway/src/helper-files/file-operation.helper.ts @@ -4,7 +4,7 @@ import * as fs from 'fs'; export const createFile = async (path: string, fileName: string, data: string): Promise => { // eslint-disable-next-line @typescript-eslint/no-use-before-define if (!checkIfFileOrDirectoryExists(path)) { - fs.mkdirSync(path); + fs.mkdirSync(path, { recursive: true }); } // eslint-disable-next-line @typescript-eslint/no-unused-vars const writeFile = promisify(fs.writeFile); diff --git a/apps/api-gateway/src/verification/interfaces/verification.interface.ts b/apps/api-gateway/src/verification/interfaces/verification.interface.ts index fc082360c..7877821fa 100644 --- a/apps/api-gateway/src/verification/interfaces/verification.interface.ts +++ b/apps/api-gateway/src/verification/interfaces/verification.interface.ts @@ -32,18 +32,10 @@ export interface IProofPresentation { isVerified: boolean; } -export interface IPresentation { +export interface IPresentation extends IProofPresentation { _tags: ITags; metadata: object; id: string; - createdAt: string; - protocolVersion: string; - state: string; - connectionId: string; - threadId: string; - autoAcceptProof: string; - updatedAt: string; - isVerified: boolean; } interface ITags { From 4e9afaf4fd04995bfbcb44fa3ec9242b87193381 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Sun, 26 Oct 2025 15:34:23 +0530 Subject: [PATCH 08/43] Feat/manage x509 cert (#1492) * added x509 cert for mdoc Signed-off-by: Rinkal Bhojani * removed commented code Signed-off-by: Rinkal Bhojani * removed commented code Signed-off-by: Rinkal Bhojani --------- Signed-off-by: Rinkal Bhojani --- .../dtos/oid4vc-issuer-template.dto.ts | 14 +-- .../interfaces/oid4vc-template.interfaces.ts | 11 +-- .../src/oid4vc-issuance.controller.ts | 2 +- .../src/oid4vc-issuance.repository.ts | 49 ++++++++++ .../src/oid4vc-issuance.service.ts | 61 ++++++++---- .../migration.sql | 94 ------------------- .../migration.sql | 15 --- .../migration.sql | 14 +++ libs/prisma-service/prisma/schema.prisma | 5 +- 9 files changed, 113 insertions(+), 152 deletions(-) delete mode 100644 libs/prisma-service/prisma/migrations/20250814141522_add_supported_protocol/migration.sql create mode 100644 libs/prisma-service/prisma/migrations/20251017081348_changed_signer_options_values/migration.sql 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 2d5bfca73..62800a31d 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 @@ -14,6 +14,7 @@ import { import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath, PartialType } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { DisplayDto } from './oid4vc-issuer.dto'; +import { SignerOption } from '@prisma/client'; export class CredentialAttributeDto { @ApiProperty({ required: false, description: 'Whether the attribute is mandatory' }) @@ -104,10 +105,6 @@ export class AppearanceDto { display: CredentialDisplayDto[]; } -export enum SignerOption { - DID = 'did', - X509 = 'x509' -} @ApiExtraModels(CredentialAttributeDto) export class CreateCredentialTemplateDto { @ApiProperty({ description: 'Template name' }) @@ -185,15 +182,6 @@ export class CreateCredentialTemplateDto { uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', alt_text: 'abc_logo' } - }, - { - locale: 'ar', - name: 'شهادة الميلاد', - description: 'سجل رسمي للولادة', - logo: { - uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg', - alt_text: 'شعار abc' - } } ] } diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts index 5d2984414..7b36b8f18 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -1,5 +1,6 @@ -import { Prisma } from '@prisma/client'; +import { Prisma, SignerOption } from '@prisma/client'; import { Display } from './oid4vc-issuance.interfaces'; +import { CredentialFormat } from '@credebl/enum/enum'; export interface CredentialAttribute { mandatory?: boolean; @@ -7,15 +8,11 @@ export interface CredentialAttribute { display?: Display[]; } -export enum SignerOption { - DID = 'did', - X509 = 'x509' -} export interface CreateCredentialTemplate { name: string; description?: string; - signerOption?: SignerOption; - format: 'sd-jwt-vc' | 'mdoc'; + signerOption?: SignerOption; //SignerOption; + format: CredentialFormat; issuer: string; canBeRevoked: boolean; attributes: Prisma.JsonValue; diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts index 72a7f4c46..1812e3c90 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts @@ -47,7 +47,7 @@ export class Oid4vcIssuanceController { } @MessagePattern({ cmd: 'oid4vc-get-issuers-issuance' }) - async oidcGetIssuers(payload: { orgId: string }): Promise { + async oidcGetIssuers(payload: { orgId: string }): Promise { const { orgId } = payload; return this.oid4vcIssuanceService.oidcIssuers(orgId); } diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts index 6c12475fb..d6906f9a3 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts @@ -5,6 +5,8 @@ import { Prisma, credential_templates, oidc_issuer, org_agents } from '@prisma/c import { PrismaService } from '@credebl/prisma-service'; import { IssuerMetadata, IssuerUpdation, OrgAgent } from '../interfaces/oid4vc-issuance.interfaces'; import { ResponseMessages } from '@credebl/common/response-messages'; +import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; @Injectable() export class Oid4vcIssuanceRepository { @@ -111,6 +113,24 @@ export class Oid4vcIssuanceRepository { } } + async getAllOidcIssuersByOrg(orgId: string): Promise { + try { + return await this.prisma.oidc_issuer.findMany({ + where: { + orgAgent: { + orgId + } + }, + orderBy: { + createDateTime: 'desc' + } + }); + } catch (error) { + this.logger.error(`Error in getOidcIssuerByOrg: ${error.message}`); + throw error; + } + } + async getOidcIssuerDetailsById(issuerId: string): Promise { try { return await this.prisma.oidc_issuer.findFirstOrThrow({ @@ -122,6 +142,35 @@ export class Oid4vcIssuanceRepository { } } + async getCurrentActiveCertificate(orgId: string, keyType: x5cKeyType): Promise { + try { + const now = new Date(); + + const certificate = await this.prisma.x509_certificates.findFirst({ + where: { + org_agents: { + orgId + }, + status: x5cRecordStatus.Active, + keyType, + validFrom: { + lte: now + }, + expiry: { + gte: now + } + }, + orderBy: { + createdAt: 'desc' + } + }); + return certificate; + } catch (error) { + this.logger.error(`Error in getCurrentActiveCertificate: ${error.message}`); + throw error; + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any async addOidcIssuerDetails(issuerMetadata: IssuerMetadata, issuerProfileJson): Promise { try { diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 676747ecc..ccb7cedb8 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -23,7 +23,7 @@ import { ResponseMessages } from '@credebl/common/response-messages'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { map } from 'rxjs'; import { getAgentUrl } from '@credebl/common/common.utils'; -import { credential_templates, oidc_issuer, user } from '@prisma/client'; +import { credential_templates, oidc_issuer, SignerOption, user } from '@prisma/client'; import { IAgentOIDCIssuerCreate, IssuerCreation, @@ -57,6 +57,7 @@ import { buildCredentialOfferUrl, CredentialOfferPayload } from '../libs/helpers/credential-sessions.builder'; +import { x5cKeyType } from '@credebl/enum/enum'; type CredentialDisplayItem = { logo?: { uri: string; alt_text?: string }; @@ -222,27 +223,15 @@ export class Oid4vcIssuanceService { } } - async oidcIssuers(orgId: string): Promise { + async oidcIssuers(orgId: string): Promise { try { const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails?.agentEndPoint) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } + const getIssuers = await this.oid4vcIssuanceRepository.getAllOidcIssuersByOrg(orgId); - const url = await getAgentUrl(agentDetails.agentEndPoint, CommonConstants.OIDC_GET_ALL_ISSUERS); - const issuersDetails = await this._oidcGetIssuers(url, orgId); - if (!issuersDetails || null == issuersDetails.response) { - throw new InternalServerErrorException('Error from agent while oidcIssuers'); - } - //TODO: Fix the response type from agent - const raw = issuersDetails.response as unknown; - const response: IssuerResponse[] = - 'string' === typeof raw ? (JSON.parse(raw) as IssuerResponse[]) : (raw as IssuerResponse[]); - - if (!Array.isArray(response)) { - throw new InternalServerErrorException('Invalid issuer payload from agent'); - } - return response; + return getIssuers; } catch (error: any) { const msg = error?.message ?? 'unknown error'; this.logger.error(`[oidcIssuers] - error in oidcIssuers: ${msg}`); @@ -294,13 +283,14 @@ export class Oid4vcIssuanceService { const metadata = { name, description, - format, + format: format.toString(), canBeRevoked, attributes, appearance: appearance ?? {}, issuerId, signerOption }; + console.log(`service - createTemplate: `, issuerId); // Persist in DB const createdTemplate = await this.oid4vcIssuanceRepository.createTemplate(issuerId, metadata); if (!createdTemplate) { @@ -473,14 +463,45 @@ export class Oid4vcIssuanceService { //TDOD: signerOption should be under credentials change this with x509 support const signerOptions = []; - getAllOfferTemplates.forEach((template) => { - if (template.signerOption === SignerMethodOption.DID) { + for (const template of getAllOfferTemplates) { + if (template.signerOption === SignerOption.DID) { signerOptions.push({ method: SignerMethodOption.DID, did: agentDetails.orgDid }); } - }); + + if (template.signerOption == SignerOption.X509_P256) { + const activeCertificate = await this.oid4vcIssuanceRepository.getCurrentActiveCertificate( + orgId, + x5cKeyType.P256 + ); + + if (!activeCertificate) { + throw new NotFoundException('No active certificate(p256) found for issuer'); + } + signerOptions.push({ + method: SignerMethodOption.X5C, + x5c: [activeCertificate.certificateBase64] + }); + } + + if (template.signerOption == SignerOption.X509_ED25519) { + const activeCertificate = await this.oid4vcIssuanceRepository.getCurrentActiveCertificate( + orgId, + x5cKeyType.Ed25519 + ); + + if (!activeCertificate) { + throw new NotFoundException('No active certificate(ed25519) found for issuer'); + } + signerOptions.push({ + method: SignerMethodOption.X5C, + x5c: [activeCertificate.certificateBase64] + }); + } + } + console.log(`Setup signerOptions `, signerOptions); //TODO: Implement x509 support and discuss with team const buildOidcCredentialOffer: CredentialOfferPayload = buildCredentialOfferPayload( createOidcCredentialOffer, diff --git a/libs/prisma-service/prisma/migrations/20250814141522_add_supported_protocol/migration.sql b/libs/prisma-service/prisma/migrations/20250814141522_add_supported_protocol/migration.sql deleted file mode 100644 index 1c42976ca..000000000 --- a/libs/prisma-service/prisma/migrations/20250814141522_add_supported_protocol/migration.sql +++ /dev/null @@ -1,94 +0,0 @@ --- Create enum type -CREATE TYPE "CredentialExchangeProtocol" AS ENUM ('OIDC', 'DIDCOMM'); - --- Add column -ALTER TABLE "organisation" -ADD COLUMN "supported_protocol" "CredentialExchangeProtocol"[]; - --- Set default for new rows -ALTER TABLE "organisation" -ALTER COLUMN "supported_protocol" -SET DEFAULT ARRAY['DIDCOMM']::"CredentialExchangeProtocol"[]; - --- Clean existing duplicates before constraint --- UPDATE "organisation" --- SET "supported_protocol" = ARRAY( --- SELECT DISTINCT unnest("supported_protocol") --- ) --- WHERE "supported_protocol" IS NOT NULL; - --- Backfill missing/empty with DIDCOMM -UPDATE "organisation" -SET "supported_protocol" = ARRAY['DIDCOMM']::"CredentialExchangeProtocol"[] -WHERE "supported_protocol" IS NULL OR cardinality("supported_protocol") = 0; - --- Add no-duplicates constraint --- ALTER TABLE "organisation" --- ADD CONSTRAINT supported_protocol_unique --- CHECK ( --- cardinality(supported_protocol) = cardinality( --- ARRAY(SELECT DISTINCT unnest(supported_protocol)) --- ) --- ); --- trigger function to check unique values -CREATE OR REPLACE FUNCTION check_unique_protocols() -RETURNS trigger AS $$ -BEGIN - IF (SELECT COUNT(*) FROM unnest(NEW.supported_protocol) v - GROUP BY v HAVING COUNT(*) > 1 LIMIT 1) IS NOT NULL THEN - RAISE EXCEPTION 'supported_protocol contains duplicates'; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER enforce_unique_protocols -BEFORE INSERT OR UPDATE ON organisation -FOR EACH ROW EXECUTE FUNCTION check_unique_protocols(); - - - --- contraints, triggers and migrations for ledger table -ALTER TABLE "ledgers" -ADD COLUMN "supported_protocol" "CredentialExchangeProtocol"[]; - --- Set default for new rows -ALTER TABLE "ledgers" -ALTER COLUMN "supported_protocol" -SET DEFAULT ARRAY['DIDCOMM']::"CredentialExchangeProtocol"[]; - --- Clean existing duplicates before constraint --- UPDATE "ledgers" --- SET "supported_protocol" = ARRAY( --- SELECT DISTINCT unnest("supported_protocol") --- ) --- WHERE "supported_protocol" IS NOT NULL; - --- Backfill missing/empty with DIDCOMM -UPDATE "ledgers" -SET "supported_protocol" = ARRAY['DIDCOMM']::"CredentialExchangeProtocol"[] -WHERE "supported_protocol" IS NULL OR cardinality("supported_protocol") = 0; - --- Add no-duplicates constraint --- ALTER TABLE "ledgers" --- ADD CONSTRAINT supported_protocol_unique --- CHECK ( --- cardinality(supported_protocol) = cardinality( --- ARRAY(SELECT DISTINCT unnest(supported_protocol)) --- ) --- ); --- trigger function to check unique values -CREATE OR REPLACE FUNCTION check_unique_protocols() -RETURNS trigger AS $$ -BEGIN - IF (SELECT COUNT(*) FROM unnest(NEW.supported_protocol) v - GROUP BY v HAVING COUNT(*) > 1 LIMIT 1) IS NOT NULL THEN - RAISE EXCEPTION 'supported_protocol contains duplicates'; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER enforce_unique_protocols -BEFORE INSERT OR UPDATE ON ledgers -FOR EACH ROW EXECUTE FUNCTION check_unique_protocols(); \ No newline at end of file diff --git a/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql b/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql index 21ce60a0f..74cd47e8e 100644 --- a/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql +++ b/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql @@ -1,18 +1,3 @@ -/* - Warnings: - - - You are about to drop the column `supported_protocol` on the `ledgers` table. All the data in the column will be lost. - - You are about to drop the column `supported_protocol` on the `organisation` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "ledgers" DROP COLUMN "supported_protocol"; - --- AlterTable -ALTER TABLE "organisation" DROP COLUMN "supported_protocol"; - --- DropEnum -DROP TYPE "CredentialExchangeProtocol"; -- CreateTable CREATE TABLE "oid4vc_credentials" ( diff --git a/libs/prisma-service/prisma/migrations/20251017081348_changed_signer_options_values/migration.sql b/libs/prisma-service/prisma/migrations/20251017081348_changed_signer_options_values/migration.sql new file mode 100644 index 000000000..3026fd028 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251017081348_changed_signer_options_values/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - The values [did,x509] on the enum `SignerOption` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "SignerOption_new" AS ENUM ('DID', 'X509_P256', 'X509_ED25519'); +ALTER TABLE "credential_templates" ALTER COLUMN "signerOption" TYPE "SignerOption_new" USING ("signerOption"::text::"SignerOption_new"); +ALTER TYPE "SignerOption" RENAME TO "SignerOption_old"; +ALTER TYPE "SignerOption_new" RENAME TO "SignerOption"; +DROP TYPE "SignerOption_old"; +COMMIT; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 495acd350..f2baa0278 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -601,8 +601,9 @@ model oid4vc_credentials { } enum SignerOption { - did - x509 + DID + X509_P256 + X509_ED25519 } model credential_templates { From 13e0c1e0fea9bccec9152209409cf4ced09371d3 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Wed, 22 Oct 2025 16:01:33 +0530 Subject: [PATCH 09/43] namespace changes Signed-off-by: Krishna Waske --- .../agent-service/dto/create-schema.dto.ts | 43 +++++++------- .../dtos/oid4vc-issuer-template.dto.ts | 12 +++- .../oid4vc-issuance/dtos/oid4vc-issuer.dto.ts | 50 ++++++++-------- .../interfaces/oid4vc-issuance.interfaces.ts | 10 ++-- .../libs/helpers/issuer.metadata.ts | 59 ++++++++----------- 5 files changed, 91 insertions(+), 83 deletions(-) diff --git a/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts index f34d802ae..21dd0cf1b 100644 --- a/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts +++ b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts @@ -2,26 +2,29 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString, IsNotEmpty, IsArray } from 'class-validator'; export class CreateTenantSchemaDto { - @ApiProperty() - @IsString({ message: 'tenantId must be a string' }) @IsNotEmpty({ message: 'please provide valid tenantId' }) - tenantId: string; - - @ApiProperty() - @IsString({ message: 'schema version must be a string' }) @IsNotEmpty({ message: 'please provide valid schema version' }) - schemaVersion: string; + @ApiProperty() + @IsString({ message: 'tenantId must be a string' }) + @IsNotEmpty({ message: 'please provide valid tenantId' }) + tenantId: string; - @ApiProperty() - @IsString({ message: 'schema name must be a string' }) @IsNotEmpty({ message: 'please provide valid schema name' }) - schemaName: string; + @ApiProperty() + @IsString({ message: 'schema version must be a string' }) + @IsNotEmpty({ message: 'please provide valid schema version' }) + schemaVersion: string; - @ApiProperty() - @IsArray({ message: 'attributes must be an array' }) - @IsString({ each: true }) - @IsNotEmpty({ message: 'please provide valid attributes' }) - attributes: string[]; + @ApiProperty() + @IsString({ message: 'schema name must be a string' }) + @IsNotEmpty({ message: 'please provide valid schema name' }) + schemaName: string; - @ApiProperty() - - @IsNotEmpty({ message: 'please provide orgId' }) - orgId: string; -} \ No newline at end of file + @ApiProperty() + @IsArray({ message: 'attributes must be an array' }) + @IsString({ each: true }) + // TODO: IsNotEmpty won't work for array. Must use @ArrayNotEmpty() instead + @IsNotEmpty({ message: 'please provide valid attributes' }) + attributes: string[]; + + @ApiProperty() + @IsNotEmpty({ message: 'please provide orgId' }) + orgId: 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 62800a31d..c4cf5c7f5 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 @@ -22,10 +22,20 @@ export class CredentialAttributeDto { @IsBoolean() mandatory?: boolean; + // TODO: Check how do we handle claims with only path rpoperty like email, etc. @ApiProperty({ description: 'Type of the attribute value (string, number, date, etc.)' }) @IsString() value_type: string; + @ApiProperty({ + type: [String], + description: + 'Claims path pointer as per the draft 15 - https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID2.html#name-claims-path-pointer' + }) + @IsArray() + @IsString({ each: true }) + path: string[]; + @ApiProperty({ type: [DisplayDto], required: false, description: 'Localized display values' }) @IsOptional() @ValidateNested({ each: true }) @@ -163,7 +173,7 @@ export class CreateCredentialTemplateDto { description: 'Attributes included in the credential template' }) @IsObject() - attributes: Record; + attributes: CredentialAttributeDto[]; @ApiProperty({ type: Object, diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts index b9e67f423..fee4af086 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -1,26 +1,27 @@ /* eslint-disable camelcase */ -import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; +import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsString, IsOptional, IsBoolean, IsArray, ValidateNested, - IsObject, IsUrl, IsNotEmpty, IsDefined, IsInt } from 'class-validator'; -import { plainToInstance, Transform, Type } from 'class-transformer'; +import { Type } from 'class-transformer'; export class ClaimDto { @ApiProperty({ - description: 'The unique key for the claim (e.g. email, name)', - example: 'email' + description: 'The path for nested claims', + example: ['address', 'street_number'], + type: [String] }) - @IsString() - key: string; + @Type(() => String) + @IsArray() + path: string[]; @ApiProperty({ description: 'The display label for the claim', @@ -85,6 +86,7 @@ export class DisplayDto { logo?: LogoDto; } +// TODO: Check where it is used, coz no reference found @ApiExtraModels(ClaimDto) export class CredentialConfigurationDto { @ApiProperty({ @@ -110,25 +112,25 @@ export class CredentialConfigurationDto { @IsString() scope: string; - // @ApiProperty({ - // description: 'List of claims supported in this credential', - // type: [ClaimDto], - // }) - // @IsArray() - // @ValidateNested({ each: true }) - // @Type(() => ClaimDto) - // claims: ClaimDto[] @ApiProperty({ - description: 'Claims supported by this credential', - type: 'object', - additionalProperties: { $ref: getSchemaPath(ClaimDto) } + description: 'List of claims supported in this credential', + type: [ClaimDto] }) - @IsObject() + @IsArray() @ValidateNested({ each: true }) - @Transform(({ value }) => - Object.fromEntries(Object.entries(value || {}).map(([k, v]) => [k, plainToInstance(ClaimDto, v)])) - ) - claims: Record; + @Type(() => ClaimDto) + claims: ClaimDto[]; + // @ApiProperty({ + // description: 'Claims supported by this credential', + // type: 'object', + // additionalProperties: { $ref: getSchemaPath(ClaimDto) } + // }) + // @IsObject() + // @ValidateNested({ each: true }) + // @Transform(({ value }) => + // Object.fromEntries(Object.entries(value || {}).map(([k, v]) => [k, plainToInstance(ClaimDto, v)])) + // ) + // claims: Record; @ApiProperty({ type: [String] }) @IsArray() @@ -217,7 +219,7 @@ export enum AccessTokenSignerKeyType { ED25519 = 'ed25519' } -@ApiExtraModels(CredentialConfigurationDto) +// @ApiExtraModels(CredentialConfigurationDto) export class IssuerCreationDto { @ApiProperty({ description: 'Name of the issuer', diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts index 1c55048af..04ff5ff3e 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts @@ -18,9 +18,9 @@ export interface OrgAgent { } export interface Claim { - key: string; - label: string; - required: boolean; + path: string[]; + label?: string; + required?: boolean; } export interface Logo { @@ -40,7 +40,7 @@ export interface CredentialConfiguration { vct?: string; doctype?: string; scope: string; - claims: Record; + claims: Claim[]; credential_signing_alg_values_supported: string[]; cryptographic_binding_methods_supported: string[]; display: Display[]; @@ -61,7 +61,7 @@ export interface IssuerCreation { accessTokenSignerKeyType?: AccessTokenSignerKeyType; display: Display[]; dpopSigningAlgValuesSupported?: string[]; - credentialConfigurationsSupported?: Record; + // credentialConfigurationsSupported?: Record; // Not used authorizationServerConfigs: AuthorizationServerConfig; batchCredentialIssuanceSize: number; } diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index 4f6bc6047..d2e126026 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -22,27 +22,25 @@ type Appearance = { display: CredentialDisplayItem[]; }; +type Claim = { + mandatory?: boolean; + // value_type: string; + path: string[]; + display?: AttributeDisplay[]; +}; + type CredentialConfig = { format: string; vct?: string; scope: string; doctype?: string; - claims: Record< - string, - { - mandatory?: boolean; - value_type: string; - display?: AttributeDisplay[]; - } - >; + claims: Claim[]; credential_signing_alg_values_supported: string[]; cryptographic_binding_methods_supported: string[]; display: { name: string; description?: string; locale?: string }[]; }; -type CredentialConfigurationsSupported = { - credentialConfigurationsSupported: Record; -}; +type CredentialConfigurationsSupported = CredentialConfig[]; // ---- Static Lists (as requested) ---- const STATIC_CREDENTIAL_ALGS = ['ES256', 'EdDSA'] as const; @@ -101,8 +99,7 @@ export function buildCredentialConfigurationsSupported( } ): CredentialConfigurationsSupported { const defaultFormat = opts?.format ?? 'vc+sd-jwt'; - const credentialConfigurationsSupported: Record = {}; - + const credentialConfigurationsSupported: CredentialConfigurationsSupported = []; for (const t of templates) { const attrs = coerceJsonObject(t.attributes); const app = coerceJsonObject(t.appearance); @@ -120,8 +117,8 @@ export function buildCredentialConfigurationsSupported( const isMdoc = 'mso_mdoc' === rowFormat; const suffix = isMdoc ? 'mdoc' : 'sdjwt'; - // key (allow override) - const key = 'function' === typeof opts?.keyResolver ? opts.keyResolver(t) : `${t.name}-${suffix}`; + // key: keep your keyResolver override; otherwise include suffix + // const key = 'function' === typeof opts?.keyResolver ? opts.keyResolver(t) : `${t.name}-${suffix}`; // Resolve doctype/vct: // - For mdoc: try opts.doctype -> t.doctype -> fallback to t.name (or throw if you prefer) @@ -143,20 +140,15 @@ export function buildCredentialConfigurationsSupported( // Choose scope base: prefer opts.scopeVct, otherwise for mdoc use doctype, else vct const scopeBase = opts?.scopeVct ?? (isMdoc ? rowDoctype : rowVct); const scope = `openid4vc:credential:${scopeBase}-${suffix}`; - - const claims = Object.fromEntries( - Object.entries(attrs).map(([claimName, def]) => { - const d = def as AttributeDef; - return [ - claimName, - { - value_type: d.value_type, - mandatory: d.mandatory ?? false, - display: Array.isArray(d.display) ? d.display.map((x) => ({ name: x.name, locale: x.locale })) : undefined - } - ]; - }) - ); + const claims = Object.entries(attrs).map(([claimName, def]) => { + const d = def as AttributeDef; + return { + path: [claimName], + // value_type: d.value_type, // Didn't find this in draft 15 + mandatory: d.mandatory ?? false, // always include, default to false + display: Array.isArray(d.display) ? d.display.map((x) => ({ name: x.name, locale: x.locale })) : undefined + }; + }); const display = app.display?.map((d) => ({ @@ -165,7 +157,8 @@ export function buildCredentialConfigurationsSupported( locale: d.locale })) ?? []; - credentialConfigurationsSupported[key] = { + // assemble per-template config + credentialConfigurationsSupported.push({ format: rowFormat, scope, claims, @@ -173,10 +166,10 @@ export function buildCredentialConfigurationsSupported( cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], display, ...(isMdoc ? { doctype: rowDoctype as string } : { vct: rowVct }) - }; + }); } - return { credentialConfigurationsSupported }; + return credentialConfigurationsSupported; } // Default DPoP list for issuer-level metadata (match your example) @@ -241,7 +234,7 @@ export function buildIssuerPayload( return { display, dpopSigningAlgValuesSupported: opts?.dpopAlgs ?? [...ISSUER_DPOP_ALGS_DEFAULT], - credentialConfigurationsSupported: credentialConfigurations.credentialConfigurationsSupported ?? {}, + credentialConfigurationsSupported: credentialConfigurations ?? [], batchCredentialIssuance: { batchSize: oidcIssuer?.batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault } From 140d0c0c55216bdc981882cc64dd597aceee3cf3 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Tue, 21 Oct 2025 17:55:19 +0530 Subject: [PATCH 10/43] added x509 cert for mdoc Signed-off-by: Rinkal Bhojani --- .../src/oid4vc-issuance.repository.ts | 3 +++ .../src/oid4vc-issuance.service.ts | 17 ++++++++++++++++- libs/enum/src/enum.ts | 6 ++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts index d6906f9a3..f4d9b829b 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts @@ -121,6 +121,9 @@ export class Oid4vcIssuanceRepository { orgId } }, + // include: { + // templates: true + // }, orderBy: { createDateTime: 'desc' } diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index ccb7cedb8..4aa65ee7f 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -223,7 +223,8 @@ export class Oid4vcIssuanceService { } } - async oidcIssuers(orgId: string): Promise { + async oidcIssuers(orgId: string): Promise { + //Promise { try { const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails?.agentEndPoint) { @@ -231,6 +232,20 @@ export class Oid4vcIssuanceService { } const getIssuers = await this.oid4vcIssuanceRepository.getAllOidcIssuersByOrg(orgId); + // const url = await getAgentUrl(agentDetails.agentEndPoint, CommonConstants.OIDC_GET_ALL_ISSUERS); + // const issuersDetails = await this._oidcGetIssuers(url, orgId); + // if (!issuersDetails || null == issuersDetails.response) { + // throw new InternalServerErrorException('Error from agent while oidcIssuers'); + // } + // //TODO: Fix the response type from agent + // const raw = issuersDetails.response as unknown; + // const response: IssuerResponse[] = + // 'string' === typeof raw ? (JSON.parse(raw) as IssuerResponse[]) : (raw as IssuerResponse[]); + + // if (!Array.isArray(response)) { + // throw new InternalServerErrorException('Invalid issuer payload from agent'); + // } + // return response; return getIssuers; } catch (error: any) { const msg = error?.message ?? 'unknown error'; diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 0a52130df..043ca4e1f 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -322,3 +322,9 @@ export enum CredentialFormat { SdJwtVc = 'vc+sd-jwt', Mdoc = 'mso_mdoc' } + +// export enum SignerOption { +// DID, +// X509_P256, +// X509_ED25519 +// } From bbacbe3dbdaef6271449624db6cecbb8cd268d48 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Sun, 26 Oct 2025 15:44:28 +0530 Subject: [PATCH 11/43] feat: seperate mdoc builder method Signed-off-by: Tipu_Singh --- .../dtos/issuer-sessions.dto.ts | 25 +- .../oid4vc-issuance/dtos/oid4vc-issuer.dto.ts | 4 + .../oid4vc-issuance.controller.ts | 2 +- .../interfaces/oid4vc-issuance.interfaces.ts | 3 +- .../credential-sessions-mdoc.builder.ts | 449 ++++++++++++++++++ .../libs/helpers/issuer.metadata.ts | 30 +- .../src/oid4vc-issuance.repository.ts | 6 +- .../src/oid4vc-issuance.service.ts | 4 +- .../prisma/data/credebl-master-table.json | 178 +++++++ .../migration.sql | 8 + libs/prisma-service/prisma/schema.prisma | 1 + 11 files changed, 681 insertions(+), 29 deletions(-) create mode 100644 apps/oid4vc-issuance/libs/helpers/credential-sessions-mdoc.builder.ts create mode 100644 libs/prisma-service/prisma/data/credebl-master-table.json create mode 100644 libs/prisma-service/prisma/migrations/20251023120134_added_authorization_server_url/migration.sql 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 77c88b075..8d9630964 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 @@ -157,25 +157,16 @@ export class CreateOidcCredentialOfferDto { @Type(() => CredentialRequestDto) credentials!: CredentialRequestDto[]; - // XOR: exactly one present - @ApiPropertyOptional({ type: PreAuthorizedCodeFlowConfigDto }) - @IsOptional() - @ValidateNested() - @Type(() => PreAuthorizedCodeFlowConfigDto) - preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfigDto; - - @IsOptional() - @ValidateNested() - @Type(() => AuthorizationCodeFlowConfigDto) - authorizationCodeFlowConfig?: AuthorizationCodeFlowConfigDto; + @ApiProperty({ + example: 'preAuthorizedCodeFlow', + enum: ['preAuthorizedCodeFlow', 'authorizationCodeFlow'], + description: 'Authorization type' + }) + @IsString() + @IsIn(['preAuthorizedCodeFlow', 'authorizationCodeFlow']) + authorizationType!: 'preAuthorizedCodeFlow' | 'authorizationCodeFlow'; issuerId?: string; - - // host XOR rule - @ExactlyOneOf(['preAuthorizedCodeFlowConfig', 'authorizationCodeFlowConfig'], { - message: 'Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.' - }) - private readonly _exactlyOne?: unknown; } export class GetAllCredentialOfferDto { diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts index fee4af086..c52ad5136 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -246,6 +246,10 @@ export class IssuerCreationDto { @Type(() => DisplayDto) display: DisplayDto[]; + @ApiProperty({ example: 'https://auth.example.org', description: 'Authorization URL' }) + @IsUrl({ require_tld: false }) + authorizationServerUrl: string; + @ApiProperty({ description: 'Configuration of the authorization server', type: AuthorizationServerConfigDto diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts index 70a86bcbf..66b050146 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts @@ -437,7 +437,7 @@ export class Oid4vcIssuanceController { @Post('/orgs/:orgId/oid4vc/:issuerId/create-offer') @ApiOperation({ summary: 'Create OID4VC Credential Offer', - description: 'Creates a new OIDC4VCI credential-offer for a given issuer.' + description: 'Creates a new OID4VC credential-offer for a given issuer.' }) @ApiResponse({ status: HttpStatus.CREATED, description: 'Credential offer created successfully.' }) @ApiBearerAuth() diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts index 04ff5ff3e..9ecefedfb 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts @@ -57,11 +57,11 @@ export interface AuthorizationServerConfig { } export interface IssuerCreation { + authorizationServerUrl: string; issuerId: string; accessTokenSignerKeyType?: AccessTokenSignerKeyType; display: Display[]; dpopSigningAlgValuesSupported?: string[]; - // credentialConfigurationsSupported?: Record; // Not used authorizationServerConfigs: AuthorizationServerConfig; batchCredentialIssuanceSize: number; } @@ -79,6 +79,7 @@ export interface IssuerInitialConfig { } export interface IssuerMetadata { + authorizationServerUrl: string; publicIssuerId: string; createdById: string; orgAgentId: string; diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions-mdoc.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions-mdoc.builder.ts new file mode 100644 index 000000000..7d892a89f --- /dev/null +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions-mdoc.builder.ts @@ -0,0 +1,449 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ +import { Prisma, credential_templates } from '@prisma/client'; +import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; +import { CredentialFormat } from '@credebl/enum/enum'; +/* ============================================================================ + Domain Types +============================================================================ */ + +type ValueType = 'string' | 'date' | 'number' | 'boolean' | 'integer' | string; + +interface TemplateAttribute { + display: { name: string; locale: string }[]; + mandatory: boolean; + value_type: ValueType; +} +type TemplateAttributes = Record; + +export enum SignerMethodOption { + DID = 'did', + X5C = 'x5c' +} + +export type DisclosureFrame = Record>; + +export interface CredentialRequestDtoLike { + templateId: string; + payload: Record; + disclosureFrame?: DisclosureFrame; +} + +export interface CreateOidcCredentialOfferDtoLike { + credentials: CredentialRequestDtoLike[]; + + // Exactly one of the two must be provided (XOR) + preAuthorizedCodeFlowConfig?: { + txCode: { description?: string; length: number; input_mode: 'numeric' | 'text' | 'alphanumeric' }; + authorizationServerUrl: string; + }; + authorizationCodeFlowConfig?: { + authorizationServerUrl: string; + }; + + // NOTE: issuerId is intentionally NOT emitted in the final payload + publicIssuerId?: string; +} + +export interface ResolvedSignerOption { + method: 'did' | 'x5c'; + did?: string; + x5c?: string[]; +} + +/* ============================================================================ + Strong return types +============================================================================ */ + +export interface BuiltCredential { + /** e.g., "BirthCertificateCredential-sdjwt" or "DrivingLicenseCredential-mdoc" */ + credentialSupportedId: string; + signerOptions?: ResolvedSignerOption; + /** Derived from template.format ("vc+sd-jwt" | "mdoc") */ + format: CredentialFormat; + /** User-provided payload (validated, with vct removed) */ + payload: Record; + /** Optional disclosure frame (usually for SD-JWT) */ + disclosureFrame?: DisclosureFrame; +} + +export interface BuiltCredentialOfferBase { + /** Resolved signer option (DID or x5c) */ + signerOption?: ResolvedSignerOption; + /** Normalized credential entries */ + credentials: BuiltCredential[]; + /** Optional public issuer id to include */ + publicIssuerId?: string; +} + +/** Final payload = base + EXACTLY ONE of the two flows */ +export type CredentialOfferPayload = BuiltCredentialOfferBase & + ( + | { + preAuthorizedCodeFlowConfig: { + txCode: { description?: string; length: number; input_mode: 'numeric' | 'text' | 'alphanumeric' }; + authorizationServerUrl: string; + }; + authorizationCodeFlowConfig?: never; + } + | { + authorizationCodeFlowConfig: { + authorizationServerUrl: string; + }; + preAuthorizedCodeFlowConfig?: never; + } + ); + +/* ============================================================================ + Small Utilities +============================================================================ */ + +const isNil = (value: unknown): value is null | undefined => null == value; +const isEmptyString = (value: unknown): boolean => 'string' === typeof value && '' === value.trim(); +const isPlainRecord = (value: unknown): value is Record => + Boolean(value) && 'object' === typeof value && !Array.isArray(value); + +/** Map DB format string -> API enum */ +function mapDbFormatToApiFormat(dbFormat: string): CredentialFormat { + if ('sd-jwt' === dbFormat || 'vc+sd-jwt' === dbFormat || 'sdjwt' === dbFormat || 'sd+jwt-vc' === dbFormat) { + return CredentialFormat.SdJwtVc; + } + if ('mso_mdoc' === dbFormat) { + return CredentialFormat.Mdoc; + } + throw new Error(`Unsupported template format: ${dbFormat}`); +} + +/** Map API enum -> id suffix required for credentialSupportedId */ +function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' { + return apiFormat === CredentialFormat.SdJwtVc ? 'sdjwt' : 'mdoc'; +} + +/* ============================================================================ + Validation of Payload vs Template Attributes +============================================================================ */ + +/** Throw if any template-mandatory claim is missing/empty in payload. */ +function assertMandatoryClaims( + payload: Record, + attributes: TemplateAttributes, + context: { templateId: string } +): void { + const missingClaims: string[] = []; + for (const [claimName, attributeDefinition] of Object.entries(attributes)) { + if (!attributeDefinition?.mandatory) { + continue; + } + const claimValue = payload[claimName]; + if (isNil(claimValue) || isEmptyString(claimValue)) { + missingClaims.push(claimName); + } + } + if (missingClaims.length) { + throw new Error(`Missing mandatory claims for template "${context.templateId}": ${missingClaims.join(', ')}`); + } +} + +/* ============================================================================ + JsonValue → TemplateAttributes Narrowing (Type Guards) +============================================================================ */ + +function isDisplayArray(value: unknown): value is { name: string; locale: string }[] { + return ( + Array.isArray(value) && + value.every( + (entry) => + isPlainRecord(entry) && 'string' === typeof (entry as any).name && 'string' === typeof (entry as any).locale + ) + ); +} + +/* ============================================================================ + Improved ensureTemplateAttributes: runtime assert with helpful errors +============================================================================ */ + +const ALLOWED_VALUE_TYPES: ValueType[] = ['string', 'date', 'number', 'boolean', 'integer']; + +function ensureTemplateAttributes(jsonValue: Prisma.JsonValue): TemplateAttributes { + if (!isPlainRecord(jsonValue)) { + throw new Error( + `Invalid template.attributes: expected an object map but received ${ + null === jsonValue ? 'null' : typeof jsonValue + }.\n\nFix: provide an object whose keys are attribute names and whose values are attribute definitions, e.g.\n{\n "given_name": { "mandatory": true, "value_type": "string" }\n}` + ); + } + + const attributesMap = jsonValue as Record; + const attributeKeys = Object.keys(attributesMap); + if (0 === attributeKeys.length) { + throw new Error( + 'Invalid template.attributes: object is empty (no attributes defined).\n\nFix: add at least one attribute definition, for example:\n{\n "given_name": { "mandatory": true, "value_type": "string" }\n}' + ); + } + + const problems: string[] = []; + const suggestedFixes: string[] = []; + + for (const attributeKey of attributeKeys) { + const rawAttributeDef = attributesMap[attributeKey]; + + if (!isPlainRecord(rawAttributeDef)) { + problems.push( + `${attributeKey}: expected an object but got ${null === rawAttributeDef ? 'null' : typeof rawAttributeDef}` + ); + suggestedFixes.push( + `Replace attribute "${attributeKey}" value with an object, e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + continue; + } + + // mandatory checks + if (!('mandatory' in rawAttributeDef)) { + problems.push(`${attributeKey}.mandatory: missing`); + suggestedFixes.push( + `Add mandatory boolean for "${attributeKey}":\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + } else if ('boolean' !== typeof (rawAttributeDef as any).mandatory) { + problems.push(`${attributeKey}.mandatory: expected boolean but got ${typeof (rawAttributeDef as any).mandatory}`); + suggestedFixes.push( + `Set "mandatory" to a boolean for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + } + + // value_type checks + if (!('value_type' in rawAttributeDef)) { + problems.push(`${attributeKey}.value_type: missing`); + suggestedFixes.push( + `Add value_type for "${attributeKey}", for example:\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + } else if ('string' !== typeof (rawAttributeDef as any).value_type) { + problems.push( + `${attributeKey}.value_type: expected string but got ${typeof (rawAttributeDef as any).value_type}` + ); + suggestedFixes.push( + `Make sure "value_type" is a string for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + } else { + const declaredType = (rawAttributeDef as any).value_type as string; + if (!ALLOWED_VALUE_TYPES.includes(declaredType as ValueType)) { + problems.push( + `${attributeKey}.value_type: unsupported value_type "${declaredType}". Allowed types: ${ALLOWED_VALUE_TYPES.join(', ')}` + ); + suggestedFixes.push( + `Use one of the allowed types for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` + ); + } + } + + // display checks (optional) + if ('display' in rawAttributeDef && !isDisplayArray((rawAttributeDef as any).display)) { + problems.push(`${attributeKey}.display: expected array of { name: string, locale: string }`); + suggestedFixes.push( + `Fix "display" for "${attributeKey}" to be an array of objects with name/locale, e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string", "display": [{ "name": "Given Name", "locale": "en-US" }] }` + ); + } + } + + if (0 < problems.length) { + // Build a user-friendly message: problems + suggested fixes (unique) + const uniqueFixes = Array.from(new Set(suggestedFixes)).slice(0, 20); + const fixesText = uniqueFixes.length + ? `\n\nSuggested fixes (copy-paste examples):\n- ${uniqueFixes.join('\n- ')}` + : ''; + + // Include a small truncated sample of the attributes to help debugging + const samplePreview = JSON.stringify( + Object.fromEntries(attributeKeys.slice(0, 10).map((key) => [key, attributesMap[key]])), + (_, value) => { + if ('string' === typeof value && 200 < value.length) { + return `${value.slice(0, 200)}...`; + } + return value; + }, + 2 + ); + + throw new Error( + `Invalid template.attributes shape. Problems found:\n- ${problems.join( + '\n- ' + )}\n\nExample attributes (truncated):\n${samplePreview}${fixesText}` + ); + } + + // Safe to cast to TemplateAttributes + return attributesMap as TemplateAttributes; +} + +/* ============================================================================ + Builders +============================================================================ */ + +/** Build one credential block normalized to API format (using the template's format). */ +function buildOneCredential( + credentialRequest: CredentialRequestDtoLike, + templateRecord: credential_templates, + templateAttributes: TemplateAttributes, + signerOptions?: SignerOption[] +): BuiltCredential { + // 1) Validate payload against template attributes + assertMandatoryClaims(credentialRequest.payload, templateAttributes, { templateId: credentialRequest.templateId }); + + // 2) Decide API format from DB format + const selectedApiFormat = mapDbFormatToApiFormat(templateRecord.format); + + // 3) Build supportedId from template.name + suffix ("-sdjwt" | "-mdoc") + const idSuffix = formatSuffix(selectedApiFormat); + const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + + // 4) Strip vct ALWAYS (per requirement) + const normalizedPayload = { ...(credentialRequest.payload as Record) }; + delete (normalizedPayload as Record).vct; + + return { + credentialSupportedId, // e.g., "BirthCertificateCredential-sdjwt" + signerOptions: signerOptions ? signerOptions[0] : undefined, + format: selectedApiFormat, // 'vc+sd-jwt' | 'mdoc' + payload: normalizedPayload, // without vct + ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) + }; +} + +/** + * Build the full OID4VC credential offer payload. + * - Verifies template IDs + * - Validates mandatory claims per template + * - Normalizes formats & IDs + * - Enforces XOR of flow configs + * - Removes issuerId from the final envelope + * - Removes vct from all payloads + * - Sets credentialSupportedId = "-sdjwt|mdoc" + */ +export function buildCredentialOfferPayload( + dto: CreateOidcCredentialOfferDtoLike, + templates: credential_templates[], + signerOptions?: SignerOption[] +): CredentialOfferPayload { + // Index templates + const templatesById = new Map(templates.map((template) => [template.id, template])); + + // Verify all requested templateIds exist + const unknownTemplateIds = dto.credentials + .map((c) => c.templateId) + .filter((requestedId) => !templatesById.has(requestedId)); + if (unknownTemplateIds.length) { + throw new Error(`Unknown template ids: ${unknownTemplateIds.join(', ')}`); + } + + // Build credentials + const builtCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { + const templateRecord = templatesById.get(credentialRequest.templateId)!; + const resolvedAttributes = ensureTemplateAttributes(templateRecord.attributes); // narrow JsonValue safely + return buildOneCredential(credentialRequest, templateRecord, resolvedAttributes, signerOptions); + }); + + // --- Base envelope (issuerId deliberately NOT included) --- + const baseEnvelope: BuiltCredentialOfferBase = { + credentials: builtCredentials, + ...(dto.publicIssuerId ? { publicIssuerId: dto.publicIssuerId } : {}) + }; + + // XOR flow selection (defensive) + const hasPreAuthFlow = Boolean(dto.preAuthorizedCodeFlowConfig); + const hasAuthCodeFlow = Boolean(dto.authorizationCodeFlowConfig); + if (hasPreAuthFlow === hasAuthCodeFlow) { + throw new Error('Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'); + } + + if (hasPreAuthFlow) { + return { + ...baseEnvelope, + preAuthorizedCodeFlowConfig: dto.preAuthorizedCodeFlowConfig! + }; + } + + return { + ...baseEnvelope, + authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! + }; +} + +// ----------------------------------------------------------------------------- +// Builder: Update Credential Offer +// ----------------------------------------------------------------------------- +export function buildUpdateCredentialOfferPayload( + dto: CreateOidcCredentialOfferDtoLike, + templates: credential_templates[] +): { credentials: BuiltCredential[] } { + // Index templates by id + const templatesById = new Map(templates.map((template) => [template.id, template])); + + // Validate all templateIds exist + const unknownTemplateIds = dto.credentials + .map((c) => c.templateId) + .filter((requestedId) => !templatesById.has(requestedId)); + if (unknownTemplateIds.length) { + throw new Error(`Unknown template ids: ${unknownTemplateIds.join(', ')}`); + } + + // Validate each credential against its template + const normalizedCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { + const templateRecord = templatesById.get(credentialRequest.templateId)!; + const resolvedAttributes = ensureTemplateAttributes(templateRecord.attributes); // safely narrow JsonValue + + // check that all payload keys exist in template attributes + const payloadKeys = Object.keys(credentialRequest.payload); + const invalidPayloadKeys = payloadKeys.filter((payloadKey) => !resolvedAttributes[payloadKey]); + if (invalidPayloadKeys.length) { + throw new Error( + `Invalid attributes for template "${credentialRequest.templateId}": ${invalidPayloadKeys.join(', ')}` + ); + } + + // also validate mandatory fields are present + assertMandatoryClaims(credentialRequest.payload, resolvedAttributes, { templateId: credentialRequest.templateId }); + + // build minimal normalized credential (no vct, issuerId, etc.) + const selectedApiFormat = mapDbFormatToApiFormat(templateRecord.format); + const idSuffix = formatSuffix(selectedApiFormat); + const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + + return { + credentialSupportedId, + format: selectedApiFormat, + payload: credentialRequest.payload, + ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) + }; + }); + + // Only return credentials array here (update flow doesn't need preAuth/auth configs) + return { + credentials: normalizedCredentials + }; +} + +export function buildCredentialOfferUrl(baseUrl: string, getAllCredentialOffer: GetAllCredentialOffer): string { + const criteriaParams: string[] = []; + + if (getAllCredentialOffer.publicIssuerId) { + criteriaParams.push(`publicIssuerId=${encodeURIComponent(getAllCredentialOffer.publicIssuerId)}`); + } + + if (getAllCredentialOffer.preAuthorizedCode) { + criteriaParams.push(`preAuthorizedCode=${encodeURIComponent(getAllCredentialOffer.preAuthorizedCode)}`); + } + + if (getAllCredentialOffer.state) { + criteriaParams.push(`state=${encodeURIComponent(getAllCredentialOffer.state)}`); + } + + if (getAllCredentialOffer.credentialOfferUri) { + criteriaParams.push(`credentialOfferUri=${encodeURIComponent(getAllCredentialOffer.credentialOfferUri)}`); + } + + if (getAllCredentialOffer.authorizationCode) { + criteriaParams.push(`authorizationCode=${encodeURIComponent(getAllCredentialOffer.authorizationCode)}`); + } + + // Append query string if any params exist + return 0 < criteriaParams.length ? `${baseUrl}?${criteriaParams.join('&')}` : baseUrl; +} diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index d2e126026..baa272cc4 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -1,4 +1,5 @@ /* eslint-disable camelcase */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { oidc_issuer, Prisma } from '@prisma/client'; import { batchCredentialIssuanceDefault } from '../../constant/issuance'; import { CreateOidcCredentialOffer } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; @@ -40,7 +41,9 @@ type CredentialConfig = { display: { name: string; description?: string; locale?: string }[]; }; -type CredentialConfigurationsSupported = CredentialConfig[]; +type CredentialConfigurationsSupported = { + credentialConfigurationsSupported: Record; +}; // ---- Static Lists (as requested) ---- const STATIC_CREDENTIAL_ALGS = ['ES256', 'EdDSA'] as const; @@ -99,7 +102,7 @@ export function buildCredentialConfigurationsSupported( } ): CredentialConfigurationsSupported { const defaultFormat = opts?.format ?? 'vc+sd-jwt'; - const credentialConfigurationsSupported: CredentialConfigurationsSupported = []; + const credentialConfigurationsSupported: Record = {}; for (const t of templates) { const attrs = coerceJsonObject(t.attributes); const app = coerceJsonObject(t.appearance); @@ -116,6 +119,7 @@ export function buildCredentialConfigurationsSupported( const rowFormat: string = (t as any).format ?? defaultFormat; const isMdoc = 'mso_mdoc' === rowFormat; const suffix = isMdoc ? 'mdoc' : 'sdjwt'; + const key = 'function' === typeof opts?.keyResolver ? opts.keyResolver(t) : `${t.name}-${suffix}`; // key: keep your keyResolver override; otherwise include suffix // const key = 'function' === typeof opts?.keyResolver ? opts.keyResolver(t) : `${t.name}-${suffix}`; @@ -158,7 +162,7 @@ export function buildCredentialConfigurationsSupported( })) ?? []; // assemble per-template config - credentialConfigurationsSupported.push({ + credentialConfigurationsSupported[key] = { format: rowFormat, scope, claims, @@ -166,10 +170,10 @@ export function buildCredentialConfigurationsSupported( cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], display, ...(isMdoc ? { doctype: rowDoctype as string } : { vct: rowVct }) - }); + }; } - return credentialConfigurationsSupported; + return { credentialConfigurationsSupported }; } // Default DPoP list for issuer-level metadata (match your example) @@ -217,7 +221,7 @@ function isDisplayArray(x: unknown): x is DisplayItem[] { */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function buildIssuerPayload( - credentialConfigurations: CredentialConfigurationsSupported, + credentialConfigurations: CredentialConfigurationsSupported | Record | null | undefined, oidcIssuer: oidc_issuer, opts?: { dpopAlgs?: string[]; @@ -231,10 +235,22 @@ export function buildIssuerPayload( const rawDisplay = coerceJson(oidcIssuer.metadata); const display: DisplayItem[] = isDisplayArray(rawDisplay) ? rawDisplay : []; + // Accept both shapes: + // 1) { credentialConfigurationsSupported: Record } + // 2) directly the Record + let credentialConfigMap: Record = {}; + if (!credentialConfigurations) { + credentialConfigMap = {}; + } else if ('credentialConfigurationsSupported' in (credentialConfigurations as any)) { + credentialConfigMap = (credentialConfigurations as any).credentialConfigurationsSupported ?? {}; + } else { + credentialConfigMap = credentialConfigurations as Record; + } + return { display, dpopSigningAlgValuesSupported: opts?.dpopAlgs ?? [...ISSUER_DPOP_ALGS_DEFAULT], - credentialConfigurationsSupported: credentialConfigurations ?? [], + credentialConfigurationsSupported: credentialConfigMap, batchCredentialIssuance: { batchSize: oidcIssuer?.batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault } diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts index f4d9b829b..e2e6a3715 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts @@ -177,14 +177,16 @@ export class Oid4vcIssuanceRepository { // eslint-disable-next-line @typescript-eslint/no-explicit-any async addOidcIssuerDetails(issuerMetadata: IssuerMetadata, issuerProfileJson): Promise { try { - const { publicIssuerId, createdById, orgAgentId, batchCredentialIssuanceSize } = issuerMetadata; + const { publicIssuerId, createdById, orgAgentId, batchCredentialIssuanceSize, authorizationServerUrl } = + issuerMetadata; const oidcIssuerDetails = await this.prisma.oidc_issuer.create({ data: { metadata: issuerProfileJson, publicIssuerId, createdBy: createdById, orgAgentId, - batchCredentialIssuanceSize + batchCredentialIssuanceSize, + authorizationServerUrl } }); diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 4aa65ee7f..cc7a9c44e 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -121,6 +121,7 @@ export class Oid4vcIssuanceService { throw new InternalServerErrorException('Issuer ID missing from agent response'); } const issuerMetadata: IssuerMetadata = { + authorizationServerUrl: issuerCreation.authorizationServerUrl, publicIssuerId: issuerIdFromAgent, createdById: userDetails.id, orgAgentId, @@ -319,6 +320,7 @@ export class Oid4vcIssuanceService { opts = { ...opts, doctype }; } const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId, opts); + console.log(`service - createTemplate: `, JSON.stringify(issuerTemplateConfig)); const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); @@ -330,6 +332,7 @@ export class Oid4vcIssuanceService { } const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_TEMPLATE, issuerDetails.publicIssuerId); const createTemplateOnAgent = await this._createOIDCTemplate(issuerTemplateConfig, url, orgId); + console.log('createTemplateOnAgent::::::::::::::', createTemplateOnAgent); if (!createTemplateOnAgent) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } @@ -622,7 +625,6 @@ export class Oid4vcIssuanceService { url, orgId ); - console.log('This is the updateCredentialOfferOnAgent:', JSON.stringify(updateCredentialOfferOnAgent)); if (!updateCredentialOfferOnAgent) { throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errorUpdateOffer); } diff --git a/libs/prisma-service/prisma/data/credebl-master-table.json b/libs/prisma-service/prisma/data/credebl-master-table.json new file mode 100644 index 000000000..f20abc6a9 --- /dev/null +++ b/libs/prisma-service/prisma/data/credebl-master-table.json @@ -0,0 +1,178 @@ +{ + "platformConfigData": { + "externalIp": "##Machine Ip Address/Domain for agent setup##", + "inboundEndpoint": "##Machine Ip Address/Domain for agent setup##", + "username": "credebl", + "sgApiKey": "###Sendgrid Key###", + "emailFrom": "##Senders Mail ID##", + "apiEndpoint": "## Platform API Ip Address##", + "tailsFileServer": "##Machine Ip Address for agent setup##" + }, + "platformAdminData": { + "firstName": "CREDEBL", + "lastName": "CREDEBL", + "email": "", + "username": "", + "password": "####Please provide encrypted password using crypto-js###", + "verificationCode": "", + "isEmailVerified": true, + "supabaseUserId": "96cef763-e106-46c1-ac78-fadf2803b11f" + }, + "platformAdminOrganizationData": { + "name": "Platform-admin", + "description": "Platform-admin", + "logoUrl": "", + "website": "", + "createdBy": "", + "lastChangedBy": "" + }, + "orgRoleData": [ + { + "name": "owner", + "description": "Organization Owner" + }, + { + "name": "admin", + "description": "Organization Admin" + }, + { + "name": "issuer", + "description": "Organization Credential Issuer" + }, + { + "name": "verifier", + "description": "Organization Credential Verifier" + }, + { + "name": "holder", + "description": "Receives credentials issued by organization" + }, + { + "name": "member", + "description": "Joins the organization as member" + }, + { + "name": "platform_admin", + "description": "To setup all the platform of the user" + } + ], + "agentTypeData": [ + { + "agent": "AFJ" + }, + { + "agent": "ACAPY" + } + ], + "orgAgentTypeData": [ + { + "agent": "DEDICATED" + }, + { + "agent": "SHARED" + } + ], + "ledgerData": [ + { + "name": "Bcovrin Testnet", + "networkType": "testnet", + "poolConfig": "https://raw.githubusercontent.com/bcgov/von-network/main/BCovrin/genesis_test", + "isActive": true, + "networkString": "testnet", + "nymTxnEndpoint": "http://test.bcovrin.vonx.io/register", + "indyNamespace": "bcovrin:testnet" + }, + { + "name": "Indicio Testnet", + "networkType": "testnet", + "poolConfig": "https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_testnet_genesis", + "isActive": true, + "networkString": "testnet", + "nymTxnEndpoint": "https://selfserve.indiciotech.io/nym", + "indyNamespace": "indicio:testnet" + }, + { + "name": "Indicio Demonet", + "networkType": "demonet", + "poolConfig": "https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_demonet_genesis", + "isActive": true, + "networkString": "demonet", + "nymTxnEndpoint": "https://selfserve.indiciotech.io/nym", + "indyNamespace": "indicio:demonet" + }, + { + "name": "Indicio Mainnet", + "networkType": "mainnet", + "poolConfig": "https://raw.githubusercontent.com/Indicio-tech/indicio-network/main/genesis_files/pool_transactions_mainnet_genesis", + "isActive": true, + "networkString": "mainnet", + "nymTxnEndpoint": "https://selfserve.indiciotech.io/nym", + "indyNamespace": "indicio:mainnet" + }, + { + "name": "Polygon Testnet", + "networkType": "testnet", + "poolConfig": "", + "isActive": true, + "networkString": "testnet", + "nymTxnEndpoint": "", + "indyNamespace": "polygon:testnet" + }, + { + "name": "Polygon Mainnet", + "networkType": "mainnet", + "poolConfig": "", + "isActive": true, + "networkString": "mainnet", + "nymTxnEndpoint": "", + "indyNamespace": "polygon:mainnet" + }, + { + "name": "NA", + "networkType": "NA", + "poolConfig": "NA", + "isActive": true, + "networkString": "NA", + "nymTxnEndpoint": "NA", + "indyNamespace": "no_ledger" + } + ], + "ledgerConfig": [ + { + "name": "indy", + "details": { + "did:indy": { + "bcovrin:testnet":"did:indy:bcovrin:testnet", + "indicio:demonet":"did:indy:indicio:demonet", + "indicio:mainnet":"did:indy:indicio:mainnet", + "indicio:testnet":"did:indy:indicio:testnet" + } + } + }, + { + "name": "polygon", + "details": { + "did:polygon": { + "mainnet":"did:polygon:mainnet", + "testnet":"did:polygon:testnet" + } + } + }, + { + "name": "noLedger", + "details": { + "did:key": "did:key", + "did:web": "did:web" + } + } + ], + + "userRoleData": [ + { + "role": "HOLDER" + }, + { + "role": "DEFAULT_USER" + } + ] +} \ No newline at end of file diff --git a/libs/prisma-service/prisma/migrations/20251023120134_added_authorization_server_url/migration.sql b/libs/prisma-service/prisma/migrations/20251023120134_added_authorization_server_url/migration.sql new file mode 100644 index 000000000..8e6325394 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251023120134_added_authorization_server_url/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `authorizationServerUrl` to the `oidc_issuer` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "oidc_issuer" ADD COLUMN "authorizationServerUrl" TEXT NOT NULL; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index f2baa0278..eebca4da4 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -578,6 +578,7 @@ model oidc_issuer { createdBy String @db.Uuid publicIssuerId String metadata Json + authorizationServerUrl String orgAgentId String @db.Uuid orgAgent org_agents @relation(fields: [orgAgentId], references: [id]) templates credential_templates[] From 74bbafe950b69abe920d5b7fe8d2cb664cae43c0 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Mon, 27 Oct 2025 12:51:59 +0530 Subject: [PATCH 12/43] feat: credential path changes Signed-off-by: Tipu_Singh --- .../dtos/oid4vc-issuer-template.dto.ts | 6 +- .../helpers/credential-sessions.builder.ts | 454 ++++++++++-------- .../libs/helpers/issuer.metadata.ts | 166 ++++--- .../src/oid4vc-issuance.service.ts | 10 +- 4 files changed, 353 insertions(+), 283 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 c4cf5c7f5..1f58372d0 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 @@ -168,11 +168,11 @@ export class CreateCredentialTemplateDto { canBeRevoked = false; @ApiProperty({ - type: 'object', - additionalProperties: { $ref: getSchemaPath(CredentialAttributeDto) }, + type: 'array', + items: { $ref: getSchemaPath(CredentialAttributeDto) }, description: 'Attributes included in the credential template' }) - @IsObject() + @IsArray() attributes: CredentialAttributeDto[]; @ApiProperty({ diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index b765bdf5a..2824940bd 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -3,6 +3,7 @@ import { Prisma, credential_templates } from '@prisma/client'; import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; import { CredentialFormat } from '@credebl/enum/enum'; + /* ============================================================================ Domain Types ============================================================================ */ @@ -10,9 +11,9 @@ import { CredentialFormat } from '@credebl/enum/enum'; type ValueType = 'string' | 'date' | 'number' | 'boolean' | 'integer' | string; interface TemplateAttribute { - display: { name: string; locale: string }[]; - mandatory: boolean; - value_type: ValueType; + display?: { name: string; locale: string }[]; + mandatory?: boolean; + value_type?: ValueType; } type TemplateAttributes = Record; @@ -24,18 +25,13 @@ export enum SignerMethodOption { export type DisclosureFrame = Record>; export interface CredentialRequestDtoLike { - /** maps to credential_templates.id (the template to use) */ templateId: string; - /** per-template claims */ payload: Record; - /** optional selective disclosure map */ disclosureFrame?: DisclosureFrame; } export interface CreateOidcCredentialOfferDtoLike { credentials: CredentialRequestDtoLike[]; - - // Exactly one of the two must be provided (XOR) preAuthorizedCodeFlowConfig?: { txCode: { description?: string; length: number; input_mode: 'numeric' | 'text' | 'alphanumeric' }; authorizationServerUrl: string; @@ -43,8 +39,6 @@ export interface CreateOidcCredentialOfferDtoLike { authorizationCodeFlowConfig?: { authorizationServerUrl: string; }; - - // NOTE: issuerId is intentionally NOT emitted in the final payload publicIssuerId?: string; } @@ -59,27 +53,19 @@ export interface ResolvedSignerOption { ============================================================================ */ export interface BuiltCredential { - /** e.g., "BirthCertificateCredential-sdjwt" or "DrivingLicenseCredential-mdoc" */ credentialSupportedId: string; signerOptions?: ResolvedSignerOption; - /** Derived from template.format ("vc+sd-jwt" | "mdoc") */ format: CredentialFormat; - /** User-provided payload (validated, with vct removed) */ payload: Record; - /** Optional disclosure frame (usually for SD-JWT) */ disclosureFrame?: DisclosureFrame; } export interface BuiltCredentialOfferBase { - /** Resolved signer option (DID or x5c) */ signerOption?: ResolvedSignerOption; - /** Normalized credential entries */ credentials: BuiltCredential[]; - /** Optional public issuer id to include */ publicIssuerId?: string; } -/** Final payload = base + EXACTLY ONE of the two flows */ export type CredentialOfferPayload = BuiltCredentialOfferBase & ( | { @@ -97,6 +83,20 @@ export type CredentialOfferPayload = BuiltCredentialOfferBase & } ); +/* ============================================================================ + Constants +============================================================================ */ + +/** + * Default txCode constant requested (used for pre-authorized flow). + * The user requested this as a constant to be used by the builder. + */ +export const DEFAULT_TXCODE = { + description: 'test abc', + length: 4, + input_mode: 'numeric' as const +}; + /* ============================================================================ Small Utilities ============================================================================ */ @@ -108,25 +108,86 @@ const isPlainRecord = (value: unknown): value is Record => /** Map DB format string -> API enum */ function mapDbFormatToApiFormat(dbFormat: string): CredentialFormat { - if ('sd-jwt' === dbFormat || 'vc+sd-jwt' === dbFormat || 'sdjwt' === dbFormat || 'sd+jwt-vc' === dbFormat) { + const normalized = (dbFormat ?? '').toLowerCase(); + if (['sd-jwt', 'vc+sd-jwt', 'sdjwt', 'sd+jwt-vc'].includes(normalized)) { return CredentialFormat.SdJwtVc; } - if ('mso_mdoc' === dbFormat) { + if ('mso_mdoc' === normalized || 'mso-mdoc' === normalized || 'mdoc' === normalized) { return CredentialFormat.Mdoc; } throw new Error(`Unsupported template format: ${dbFormat}`); } -/** Map API enum -> id suffix required for credentialSupportedId */ function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' { return apiFormat === CredentialFormat.SdJwtVc ? 'sdjwt' : 'mdoc'; } /* ============================================================================ - Validation of Payload vs Template Attributes + Template Attributes Normalization + - draft-13 used map: { given_name: { mandatory:true, value_type: "string" } } + - draft-15 returns attributes as array of attribute objects (with path) + This helper accepts both and normalizes to TemplateAttributes map. +============================================================================ */ + +/** + * Normalize attributes from DB/template into TemplateAttributes map. + * Accepts: + * - map: Record + * - array: Array<{ path: string[], mandatory?: boolean, value_type?: string, display?: ... }> + */ +function normalizeTemplateAttributes(rawAttributes: Prisma.JsonValue): TemplateAttributes { + // if already a plain record keyed by claim name, cast and return + if (isPlainRecord(rawAttributes) && !Array.isArray(rawAttributes)) { + // We still guard that values look like TemplateAttribute, but be permissive. + return rawAttributes as TemplateAttributes; + } + + // If attributes are an array (draft-15 style), convert to map + if (Array.isArray(rawAttributes)) { + const attributesArray = rawAttributes as unknown as any[]; + const normalizedMap: TemplateAttributes = {}; + for (const attributeEntry of attributesArray) { + if (!isPlainRecord(attributeEntry)) { + continue; // skip invalid entries + } + + // draft-15: path is array like ["org.iso.23220.photoID.1","given_name"] or ["name"] + const pathValue = attributeEntry.path; + if (!Array.isArray(pathValue) || 0 === pathValue.length) { + continue; + } + + // prefer last path element as local claim name (keeps namespace support) + const claimName = String(pathValue[pathValue.length - 1]); + + normalizedMap[claimName] = { + mandatory: Boolean(attributeEntry.mandatory), + value_type: attributeEntry.value_type ? String(attributeEntry.value_type) : undefined, + display: Array.isArray(attributeEntry.display) + ? attributeEntry.display.map((d: any) => ({ name: d.name, locale: d.locale })) + : undefined + }; + } + return normalizedMap; + } + + // if it's a JSON string, try parse + if ('string' === typeof rawAttributes) { + try { + const parsed = JSON.parse(rawAttributes); + return normalizeTemplateAttributes(parsed as Prisma.JsonValue); + } catch { + throw new Error('Invalid template.attributes JSON string'); + } + } + + throw new Error('Unrecognized template.attributes shape'); +} + +/* ============================================================================ + Validation: Mandatory claims ============================================================================ */ -/** Throw if any template-mandatory claim is missing/empty in payload. */ function assertMandatoryClaims( payload: Record, attributes: TemplateAttributes, @@ -148,264 +209,248 @@ function assertMandatoryClaims( } /* ============================================================================ - JsonValue → TemplateAttributes Narrowing (Type Guards) + Per-format credential builders (separated for readability) + - buildSdJwtCredential + - buildMdocCredential ============================================================================ */ -function isDisplayArray(value: unknown): value is { name: string; locale: string }[] { - return ( - Array.isArray(value) && - value.every( - (entry) => - isPlainRecord(entry) && 'string' === typeof (entry as any).name && 'string' === typeof (entry as any).locale - ) - ); -} +/** Build an SD-JWT credential object */ +function buildSdJwtCredential( + credentialRequest: CredentialRequestDtoLike, + templateRecord: credential_templates, + signerOptions?: SignerOption[] +): BuiltCredential { + // For SD-JWT format we expect payload to be a flat map of claims (no namespaces) + const payloadCopy = { ...(credentialRequest.payload as Record) }; + // Validate mandatory claims using normalized attributes from templateRecord + const normalizedAttributes = normalizeTemplateAttributes(templateRecord.attributes); + assertMandatoryClaims(payloadCopy, normalizedAttributes, { templateId: credentialRequest.templateId }); -/* ============================================================================ - Improved ensureTemplateAttributes: runtime assert with helpful errors -============================================================================ */ + // strip vct if present per requirement + delete payloadCopy.vct; -const ALLOWED_VALUE_TYPES: ValueType[] = ['string', 'date', 'number', 'boolean', 'integer']; + const apiFormat = mapDbFormatToApiFormat(templateRecord.format); + const idSuffix = formatSuffix(apiFormat); + const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; -function ensureTemplateAttributes(jsonValue: Prisma.JsonValue): TemplateAttributes { - if (!isPlainRecord(jsonValue)) { - throw new Error( - `Invalid template.attributes: expected an object map but received ${ - null === jsonValue ? 'null' : typeof jsonValue - }.\n\nFix: provide an object whose keys are attribute names and whose values are attribute definitions, e.g.\n{\n "given_name": { "mandatory": true, "value_type": "string" }\n}` - ); - } + return { + credentialSupportedId, + signerOptions: signerOptions ? signerOptions[0] : undefined, + format: apiFormat, + payload: payloadCopy, + ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) + }; +} - const attributesMap = jsonValue as Record; - const attributeKeys = Object.keys(attributesMap); - if (0 === attributeKeys.length) { - throw new Error( - 'Invalid template.attributes: object is empty (no attributes defined).\n\nFix: add at least one attribute definition, for example:\n{\n "given_name": { "mandatory": true, "value_type": "string" }\n}' - ); +/** Build an MSO mdoc credential object + * - For mdocs we expect the payload to include a `namespaces` map (draft-15 style) + */ +function buildMdocCredential( + credentialRequest: CredentialRequestDtoLike, + templateRecord: credential_templates, + signerOptions?: SignerOption[] +): BuiltCredential { + const incomingPayload = { ...(credentialRequest.payload as Record) }; + + // Normalize attributes and ensure we know the expected claim names + const normalizedAttributes = normalizeTemplateAttributes(templateRecord.attributes); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const templateDoctype: string | undefined = (templateRecord as any).doctype ?? undefined; + const defaultNamespace = templateDoctype ?? templateRecord.name; + + // If caller provided already-namespaced payload, keep it; otherwise build a namespaces map + const workingPayload = { ...incomingPayload }; + if (!workingPayload.namespaces) { + const namespacesMap: Record> = {}; + // collect claims that match attribute names into the chosen namespace + for (const claimName of Object.keys(normalizedAttributes)) { + if (Object.prototype.hasOwnProperty.call(incomingPayload, claimName)) { + namespacesMap[defaultNamespace] = namespacesMap[defaultNamespace] ?? {}; + namespacesMap[defaultNamespace][claimName] = (incomingPayload as any)[claimName]; + // remove original flattened claim to avoid duplication + delete (workingPayload as any)[claimName]; + } + } + if (0 < Object.keys(namespacesMap).length) { + (workingPayload as any).namespaces = namespacesMap; + } + } else { + // ensure namespaces is a plain object + if (!isPlainRecord((workingPayload as any).namespaces)) { + throw new Error(`Invalid mdoc payload: 'namespaces' must be an object`); + } } - const problems: string[] = []; - const suggestedFixes: string[] = []; - - for (const attributeKey of attributeKeys) { - const rawAttributeDef = attributesMap[attributeKey]; - - if (!isPlainRecord(rawAttributeDef)) { - problems.push( - `${attributeKey}: expected an object but got ${null === rawAttributeDef ? 'null' : typeof rawAttributeDef}` - ); - suggestedFixes.push( - `Replace attribute "${attributeKey}" value with an object, e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); + // Validate mandatory claims exist somewhere inside namespaces + const missingMandatoryClaims: string[] = []; + for (const [claimName, attributeDef] of Object.entries(normalizedAttributes)) { + if (!attributeDef?.mandatory) { continue; } - // mandatory checks - if (!('mandatory' in rawAttributeDef)) { - problems.push(`${attributeKey}.mandatory: missing`); - suggestedFixes.push( - `Add mandatory boolean for "${attributeKey}":\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); - } else if ('boolean' !== typeof (rawAttributeDef as any).mandatory) { - problems.push(`${attributeKey}.mandatory: expected boolean but got ${typeof (rawAttributeDef as any).mandatory}`); - suggestedFixes.push( - `Set "mandatory" to a boolean for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); - } - - // value_type checks - if (!('value_type' in rawAttributeDef)) { - problems.push(`${attributeKey}.value_type: missing`); - suggestedFixes.push( - `Add value_type for "${attributeKey}", for example:\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); - } else if ('string' !== typeof (rawAttributeDef as any).value_type) { - problems.push( - `${attributeKey}.value_type: expected string but got ${typeof (rawAttributeDef as any).value_type}` - ); - suggestedFixes.push( - `Make sure "value_type" is a string for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); - } else { - const declaredType = (rawAttributeDef as any).value_type as string; - if (!ALLOWED_VALUE_TYPES.includes(declaredType as ValueType)) { - problems.push( - `${attributeKey}.value_type: unsupported value_type "${declaredType}". Allowed types: ${ALLOWED_VALUE_TYPES.join(', ')}` - ); - suggestedFixes.push( - `Use one of the allowed types for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); + let found = false; + const namespacesObj = (workingPayload as any).namespaces as Record; + if (namespacesObj && isPlainRecord(namespacesObj)) { + for (const nsKey of Object.keys(namespacesObj)) { + const nsContent = namespacesObj[nsKey]; + if (nsContent && Object.prototype.hasOwnProperty.call(nsContent, claimName)) { + const value = nsContent[claimName]; + if (!isNil(value) && !('string' === typeof value && '' === value.trim())) { + found = true; + break; + } + } } } - - // display checks (optional) - if ('display' in rawAttributeDef && !isDisplayArray((rawAttributeDef as any).display)) { - problems.push(`${attributeKey}.display: expected array of { name: string, locale: string }`); - suggestedFixes.push( - `Fix "display" for "${attributeKey}" to be an array of objects with name/locale, e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string", "display": [{ "name": "Given Name", "locale": "en-US" }] }` - ); + if (!found) { + missingMandatoryClaims.push(claimName); } } - - if (0 < problems.length) { - // Build a user-friendly message: problems + suggested fixes (unique) - const uniqueFixes = Array.from(new Set(suggestedFixes)).slice(0, 20); - const fixesText = uniqueFixes.length - ? `\n\nSuggested fixes (copy-paste examples):\n- ${uniqueFixes.join('\n- ')}` - : ''; - - // Include a small truncated sample of the attributes to help debugging - const samplePreview = JSON.stringify( - Object.fromEntries(attributeKeys.slice(0, 10).map((key) => [key, attributesMap[key]])), - (_, value) => { - if ('string' === typeof value && 200 < value.length) { - return `${value.slice(0, 200)}...`; - } - return value; - }, - 2 - ); - + if (missingMandatoryClaims.length) { throw new Error( - `Invalid template.attributes shape. Problems found:\n- ${problems.join( - '\n- ' - )}\n\nExample attributes (truncated):\n${samplePreview}${fixesText}` + `Missing mandatory namespaced claims for template "${credentialRequest.templateId}": ${missingMandatoryClaims.join( + ', ' + )}` ); } - // Safe to cast to TemplateAttributes - return attributesMap as TemplateAttributes; -} - -/* ============================================================================ - Builders -============================================================================ */ - -/** Build one credential block normalized to API format (using the template's format). */ -function buildOneCredential( - credentialRequest: CredentialRequestDtoLike, - templateRecord: credential_templates, - templateAttributes: TemplateAttributes, - signerOptions?: SignerOption[] -): BuiltCredential { - // 1) Validate payload against template attributes - assertMandatoryClaims(credentialRequest.payload, templateAttributes, { templateId: credentialRequest.templateId }); + // strip vct if present + delete (workingPayload as Record).vct; - // 2) Decide API format from DB format - const selectedApiFormat = mapDbFormatToApiFormat(templateRecord.format); - - // 3) Build supportedId from template.name + suffix ("-sdjwt" | "-mdoc") - const idSuffix = formatSuffix(selectedApiFormat); + const apiFormat = mapDbFormatToApiFormat(templateRecord.format); + const idSuffix = formatSuffix(apiFormat); const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; - // 4) Strip vct ALWAYS (per requirement) - const normalizedPayload = { ...(credentialRequest.payload as Record) }; - delete (normalizedPayload as Record).vct; - return { - credentialSupportedId, // e.g., "BirthCertificateCredential-sdjwt" + credentialSupportedId, signerOptions: signerOptions ? signerOptions[0] : undefined, - format: selectedApiFormat, // 'vc+sd-jwt' | 'mdoc' - payload: normalizedPayload, // without vct + format: apiFormat, + payload: workingPayload, ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) }; } -/** - * Build the full OID4VC credential offer payload. - * - Verifies template IDs - * - Validates mandatory claims per template - * - Normalizes formats & IDs - * - Enforces XOR of flow configs - * - Removes issuerId from the final envelope - * - Removes vct from all payloads - * - Sets credentialSupportedId = "-sdjwt|mdoc" - */ +/* ============================================================================ + Main Builder: buildCredentialOfferPayload + - Now delegates per-format build to the two helpers above + - Accepts `authorizationServerUrl` parameter; txCode is a constant above +============================================================================ */ + export function buildCredentialOfferPayload( dto: CreateOidcCredentialOfferDtoLike, templates: credential_templates[], + issuerDetails?: { + publicId: string; + authorizationServerUrl?: string; + }, signerOptions?: SignerOption[] ): CredentialOfferPayload { - // Index templates + // Index templates by id const templatesById = new Map(templates.map((template) => [template.id, template])); - // Verify all requested templateIds exist - const unknownTemplateIds = dto.credentials - .map((c) => c.templateId) - .filter((requestedId) => !templatesById.has(requestedId)); - if (unknownTemplateIds.length) { - throw new Error(`Unknown template ids: ${unknownTemplateIds.join(', ')}`); + // Validate template ids + const missingTemplateIds = dto.credentials.map((c) => c.templateId).filter((id) => !templatesById.has(id)); + if (missingTemplateIds.length) { + throw new Error(`Unknown template ids: ${missingTemplateIds.join(', ')}`); } - // Build credentials + // Build each credential using the template's format const builtCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { const templateRecord = templatesById.get(credentialRequest.templateId)!; - const resolvedAttributes = ensureTemplateAttributes(templateRecord.attributes); // narrow JsonValue safely - return buildOneCredential(credentialRequest, templateRecord, resolvedAttributes, signerOptions); + // we normalize attributes to support both draft-13 (map) and draft-15 (array) shapes + normalizeTemplateAttributes(templateRecord.attributes); + + const templateFormat = (templateRecord as any).format ?? 'vc+sd-jwt'; + const apiFormat = mapDbFormatToApiFormat(templateFormat); + + if (apiFormat === CredentialFormat.SdJwtVc) { + return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions); + } + if (apiFormat === CredentialFormat.Mdoc) { + return buildMdocCredential(credentialRequest, templateRecord, signerOptions); + } + throw new Error(`Unsupported template format for ${templateFormat}`); }); - // --- Base envelope (issuerId deliberately NOT included) --- + // Base envelope: allow explicit publicIssuerId from DTO or fallback to issuerDetails.publicId + const publicIssuerIdFromDto = dto.publicIssuerId; + const publicIssuerIdFromIssuerDetails = issuerDetails?.publicId; + const finalPublicIssuerId = publicIssuerIdFromDto ?? publicIssuerIdFromIssuerDetails; + const baseEnvelope: BuiltCredentialOfferBase = { credentials: builtCredentials, - ...(dto.publicIssuerId ? { publicIssuerId: dto.publicIssuerId } : {}) + ...(finalPublicIssuerId ? { publicIssuerId: finalPublicIssuerId } : {}) }; - // XOR flow selection (defensive) - const hasPreAuthFlow = Boolean(dto.preAuthorizedCodeFlowConfig); - const hasAuthCodeFlow = Boolean(dto.authorizationCodeFlowConfig); - if (hasPreAuthFlow === hasAuthCodeFlow) { - throw new Error('Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'); + // Determine which authorization flow to return: + // Priority: + // 1) If issuerDetails.authorizationServerUrl is provided, return preAuthorizedCodeFlowConfig using DEFAULT_TXCODE + // 2) Else fall back to flows present in DTO (still enforce XOR) + const overrideAuthorizationServerUrl = issuerDetails?.authorizationServerUrl; + if (overrideAuthorizationServerUrl) { + if ('string' !== typeof overrideAuthorizationServerUrl || '' === overrideAuthorizationServerUrl.trim()) { + throw new Error('issuerDetails.authorizationServerUrl must be a non-empty string when provided'); + } + return { + ...baseEnvelope, + preAuthorizedCodeFlowConfig: { + txCode: DEFAULT_TXCODE, + authorizationServerUrl: overrideAuthorizationServerUrl + } + }; } - if (hasPreAuthFlow) { + // No override provided — use what DTO carries (must be XOR) + const hasPreAuthFromDto = Boolean(dto.preAuthorizedCodeFlowConfig); + const hasAuthCodeFromDto = Boolean(dto.authorizationCodeFlowConfig); + if (hasPreAuthFromDto === hasAuthCodeFromDto) { + throw new Error('Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'); + } + if (hasPreAuthFromDto) { return { ...baseEnvelope, preAuthorizedCodeFlowConfig: dto.preAuthorizedCodeFlowConfig! }; } - return { ...baseEnvelope, authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! }; } -// ----------------------------------------------------------------------------- -// Builder: Update Credential Offer -// ----------------------------------------------------------------------------- +/* ============================================================================ + Update Credential Offer builder (keeps behavior, clearer names) +============================================================================ */ + export function buildUpdateCredentialOfferPayload( dto: CreateOidcCredentialOfferDtoLike, templates: credential_templates[] ): { credentials: BuiltCredential[] } { - // Index templates by id const templatesById = new Map(templates.map((template) => [template.id, template])); - // Validate all templateIds exist - const unknownTemplateIds = dto.credentials - .map((c) => c.templateId) - .filter((requestedId) => !templatesById.has(requestedId)); - if (unknownTemplateIds.length) { - throw new Error(`Unknown template ids: ${unknownTemplateIds.join(', ')}`); + const missingTemplateIds = dto.credentials.map((c) => c.templateId).filter((id) => !templatesById.has(id)); + if (missingTemplateIds.length) { + throw new Error(`Unknown template ids: ${missingTemplateIds.join(', ')}`); } - // Validate each credential against its template const normalizedCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { const templateRecord = templatesById.get(credentialRequest.templateId)!; - const resolvedAttributes = ensureTemplateAttributes(templateRecord.attributes); // safely narrow JsonValue - // check that all payload keys exist in template attributes + // Normalize attributes shape and ensure it's valid + const attributesMap = normalizeTemplateAttributes(templateRecord.attributes); + + // ensure payload keys match known attributes const payloadKeys = Object.keys(credentialRequest.payload); - const invalidPayloadKeys = payloadKeys.filter((payloadKey) => !resolvedAttributes[payloadKey]); + const invalidPayloadKeys = payloadKeys.filter((payloadKey) => !attributesMap[payloadKey]); if (invalidPayloadKeys.length) { throw new Error( `Invalid attributes for template "${credentialRequest.templateId}": ${invalidPayloadKeys.join(', ')}` ); } - // also validate mandatory fields are present - assertMandatoryClaims(credentialRequest.payload, resolvedAttributes, { templateId: credentialRequest.templateId }); + // Validate mandatory claims + assertMandatoryClaims(credentialRequest.payload, attributesMap, { templateId: credentialRequest.templateId }); - // build minimal normalized credential (no vct, issuerId, etc.) const selectedApiFormat = mapDbFormatToApiFormat(templateRecord.format); const idSuffix = formatSuffix(selectedApiFormat); const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; @@ -418,7 +463,6 @@ export function buildUpdateCredentialOfferPayload( }; }); - // Only return credentials array here (update flow doesn't need preAuth/auth configs) return { credentials: normalizedCredentials }; diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index baa272cc4..1e9bfda49 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -1,5 +1,4 @@ /* eslint-disable camelcase */ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { oidc_issuer, Prisma } from '@prisma/client'; import { batchCredentialIssuanceDefault } from '../../constant/issuance'; import { CreateOidcCredentialOffer } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; @@ -65,7 +64,7 @@ function coerceJsonObject(v: Prisma.JsonValue): T | null { } // eslint-disable-next-line @typescript-eslint/no-explicit-any function isAttributesMap(x: any): x is AttributesMap { - return x && 'object' === typeof x && !Array.isArray(x); + return x && 'object' === typeof x && Array.isArray(x); } // eslint-disable-next-line @typescript-eslint/no-explicit-any function isAppearance(x: any): x is Appearance { @@ -92,88 +91,121 @@ type TemplateRowPrisma = { * Safely coerces JSON and then builds the same structure as Builder #2. */ export function buildCredentialConfigurationsSupported( - templates: TemplateRowPrisma[], - opts?: { + templateRows: TemplateRowPrisma[], + options?: { vct?: string; doctype?: string; scopeVct?: string; - keyResolver?: (t: TemplateRowPrisma) => string; + keyResolver?: (templateRow: TemplateRowPrisma) => string; format?: string; } -): CredentialConfigurationsSupported { - const defaultFormat = opts?.format ?? 'vc+sd-jwt'; - const credentialConfigurationsSupported: Record = {}; - for (const t of templates) { - const attrs = coerceJsonObject(t.attributes); - const app = coerceJsonObject(t.appearance); - - if (!isAttributesMap(attrs)) { - throw new Error(`Template ${t.id}: invalid attributes JSON`); +): Record { + const defaultFormat = options?.format ?? 'vc+sd-jwt'; + const credentialConfigMap: Record = {}; + + for (const templateRow of templateRows) { + // Extract and validate attributes (claims) and appearance (display configuration) + const attributesJson = templateRow.attributes; + const appearanceJson = coerceJsonObject(templateRow.appearance); + + if (!isAttributesMap(attributesJson)) { + throw new Error(`Template ${templateRow.id}: invalid attributes JSON`); } - if (!isAppearance(app)) { - throw new Error(`Template ${t.id}: invalid appearance JSON (missing display)`); + + if (!isAppearance(appearanceJson)) { + throw new Error(`Template ${templateRow.id}: invalid appearance JSON (missing display array)`); } - // per-row format (allow column override) + // Determine credential format (either sd-jwt or mso_mdoc) // eslint-disable-next-line @typescript-eslint/no-explicit-any - const rowFormat: string = (t as any).format ?? defaultFormat; - const isMdoc = 'mso_mdoc' === rowFormat; - const suffix = isMdoc ? 'mdoc' : 'sdjwt'; - const key = 'function' === typeof opts?.keyResolver ? opts.keyResolver(t) : `${t.name}-${suffix}`; + const templateFormat: string = (templateRow as any).format ?? defaultFormat; + const isMdocFormat = 'mso_mdoc' === templateFormat; + const formatSuffix = isMdocFormat ? 'mdoc' : 'sdjwt'; - // key: keep your keyResolver override; otherwise include suffix - // const key = 'function' === typeof opts?.keyResolver ? opts.keyResolver(t) : `${t.name}-${suffix}`; + // Determine the unique key for this credential configuration + const configKey = + 'function' === typeof options?.keyResolver + ? options.keyResolver(templateRow) + : `${templateRow.name}-${formatSuffix}`; - // Resolve doctype/vct: - // - For mdoc: try opts.doctype -> t.doctype -> fallback to t.name (or throw if you prefer) - // - For sd-jwt: try opts.vct -> t.vct -> fallback to t.name + // Resolve Doctype and VCT based on format type // eslint-disable-next-line @typescript-eslint/no-explicit-any - let rowDoctype: string | undefined = opts?.doctype ?? (t as any).doctype; + let resolvedDoctype: string | undefined = options?.doctype ?? (templateRow as any).doctype; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const rowVct: string = opts?.vct ?? (t as any).vct ?? t.name; - - if (isMdoc) { - if (!rowDoctype) { - // Fallback strategy: use template's name as doctype (change to throw if you want strictness) - rowDoctype = t.name; - // If you want to fail instead of fallback, uncomment next line: - // throw new Error(`Template ${t.id}: doctype is required for mdoc format`); - } + const resolvedVct: string = options?.vct ?? (templateRow as any).vct ?? templateRow.name; + + if (isMdocFormat && !resolvedDoctype) { + resolvedDoctype = templateRow.name; // fallback to template name } - // Choose scope base: prefer opts.scopeVct, otherwise for mdoc use doctype, else vct - const scopeBase = opts?.scopeVct ?? (isMdoc ? rowDoctype : rowVct); - const scope = `openid4vc:credential:${scopeBase}-${suffix}`; - const claims = Object.entries(attrs).map(([claimName, def]) => { - const d = def as AttributeDef; - return { + // Construct OIDC4VC scope + const scopeBaseValue = options?.scopeVct ?? (isMdocFormat ? resolvedDoctype : resolvedVct); + const credentialScope = `openid4vc:credential:${scopeBaseValue}-${formatSuffix}`; + + // Convert each attribute into a claim definition (map shape) + const claimsObject: Record = {}; + for (const [claimName, attributeDefinition] of Object.entries(attributesJson)) { + const parsedAttribute = attributeDefinition as AttributeDef; + + claimsObject[claimName] = { path: [claimName], - // value_type: d.value_type, // Didn't find this in draft 15 - mandatory: d.mandatory ?? false, // always include, default to false - display: Array.isArray(d.display) ? d.display.map((x) => ({ name: x.name, locale: x.locale })) : undefined + mandatory: parsedAttribute.mandatory ?? false, + display: Array.isArray(parsedAttribute.display) + ? parsedAttribute.display.map((displayItem) => ({ + name: displayItem.name, + locale: displayItem.locale + })) + : undefined, + value_type: parsedAttribute.value_type }; - }); + } - const display = - app.display?.map((d) => ({ - name: d.name, - description: d.description, - locale: d.locale + // Prepare the display configuration + const displayConfigurations = + (appearanceJson as 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 })) ?? []; - // assemble per-template config - credentialConfigurationsSupported[key] = { - format: rowFormat, - scope, - claims, + // Assemble final credential configuration + credentialConfigMap[configKey] = { + format: templateFormat, + scope: credentialScope, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + claims: Object.entries(claimsObject).map(([claimName, claimDef]) => ({ + path: claimDef.path, + mandatory: claimDef.mandatory, + display: claimDef.display + // you can optionally expose claimDef.value_type here if your API schema allows + })), credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], - display, - ...(isMdoc ? { doctype: rowDoctype as string } : { vct: rowVct }) + display: displayConfigurations, + ...(isMdocFormat ? { doctype: resolvedDoctype as string } : { vct: resolvedVct }) }; } - return { credentialConfigurationsSupported }; + return credentialConfigMap; // ✅ Return flat map, not nested object +} + +/** + * Helper — Optional + * Wraps the credential configurations map into the expected schema + * for issuer metadata JSON: + * + * { "credentialConfigurationsSupported": { ... } } + */ +export function wrapCredentialConfigurationsSupported( + configsMap: Record +): CredentialConfigurationsSupported { + return { credentialConfigurationsSupported: configsMap }; } // Default DPoP list for issuer-level metadata (match your example) @@ -221,7 +253,7 @@ function isDisplayArray(x: unknown): x is DisplayItem[] { */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function buildIssuerPayload( - credentialConfigurations: CredentialConfigurationsSupported | Record | null | undefined, + credentialConfigurations: CredentialConfigurationsSupported, oidcIssuer: oidc_issuer, opts?: { dpopAlgs?: string[]; @@ -235,22 +267,10 @@ export function buildIssuerPayload( const rawDisplay = coerceJson(oidcIssuer.metadata); const display: DisplayItem[] = isDisplayArray(rawDisplay) ? rawDisplay : []; - // Accept both shapes: - // 1) { credentialConfigurationsSupported: Record } - // 2) directly the Record - let credentialConfigMap: Record = {}; - if (!credentialConfigurations) { - credentialConfigMap = {}; - } else if ('credentialConfigurationsSupported' in (credentialConfigurations as any)) { - credentialConfigMap = (credentialConfigurations as any).credentialConfigurationsSupported ?? {}; - } else { - credentialConfigMap = credentialConfigurations as Record; - } - return { display, dpopSigningAlgValuesSupported: opts?.dpopAlgs ?? [...ISSUER_DPOP_ALGS_DEFAULT], - credentialConfigurationsSupported: credentialConfigMap, + credentialConfigurationsSupported: credentialConfigurations ?? [], batchCredentialIssuance: { batchSize: oidcIssuer?.batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault } diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index cc7a9c44e..3cae5052c 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -290,6 +290,7 @@ export class Oid4vcIssuanceService { issuerId: string ): Promise { try { + //TODO: add revert mechanism if agent call fails const { name, description, format, canBeRevoked, attributes, appearance, signerOption, vct, doctype } = CredentialTemplate; const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer(name, issuerId); @@ -519,12 +520,17 @@ export class Oid4vcIssuanceService { }); } } - console.log(`Setup signerOptions `, signerOptions); //TODO: Implement x509 support and discuss with team + //TODO: add logic to pass the issuer info const buildOidcCredentialOffer: CredentialOfferPayload = buildCredentialOfferPayload( createOidcCredentialOffer, + // getAllOfferTemplates, - signerOptions + { + publicId: 'photoIdIssuer', + authorizationServerUrl: 'http://localhost:4002/oid4vci/photoIdIssuer' + }, + signerOptions as any ); console.log('This is the buildOidcCredentialOffer:', JSON.stringify(buildOidcCredentialOffer, null, 2)); From 39d244b8d10b1bd4e276bbafc9702e85f46d8dd2 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Mon, 27 Oct 2025 23:18:56 +0530 Subject: [PATCH 13/43] fix: added support for nested attribute and updated builder function Signed-off-by: Rinkal Bhojani --- .../dtos/oid4vc-issuer-template.dto.ts | 207 +++++++-- .../oid4vc-issuance/dtos/oid4vc-issuer.dto.ts | 173 +++----- .../interfaces/oid4vc-issuance.interfaces.ts | 18 +- .../interfaces/oid4vc-template.interfaces.ts | 51 ++- .../libs/helpers/issuer.metadata.ts | 418 +++++++++++++----- .../src/oid4vc-issuance.service.ts | 43 +- 6 files changed, 590 insertions(+), 320 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 1f58372d0..f3578d4f4 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 @@ -9,40 +9,100 @@ import { IsNotEmpty, IsArray, ValidateIf, - IsEmpty + IsEmpty, + ArrayNotEmpty } from 'class-validator'; import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath, PartialType } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { DisplayDto } from './oid4vc-issuer.dto'; import { SignerOption } from '@prisma/client'; +import { CredentialFormat } from '@credebl/enum/enum'; + +class CredentialAttributeDisplayDto { + @ApiPropertyOptional({ example: 'First Name' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiPropertyOptional({ example: 'en' }) + @IsString() + @IsOptional() + locale?: string; +} + +// export class CredentialAttributeDto { +// @ApiProperty({ required: false, description: 'Whether the attribute is mandatory' }) +// @IsOptional() +// @IsBoolean() +// mandatory?: boolean; + +// // TODO: Check how do we handle claims with only path rpoperty like email, etc. +// @ApiProperty({ description: 'Type of the attribute value (string, number, date, etc.)' }) +// @IsString() +// value_type: string; + +// @ApiProperty({ +// type: [String], +// description: +// 'Claims path pointer as per the draft 15 - https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID2.html#name-claims-path-pointer' +// }) +// @IsArray() +// @IsString({ each: true }) +// path: string[]; + +// @ApiProperty({ type: [CredentialAttributeDisplayDto], required: false, description: 'Localized display values' }) +// @IsOptional() +// @ValidateNested({ each: true }) +// @Type(() => CredentialAttributeDisplayDto) +// display?: CredentialAttributeDisplayDto[]; +// } + +export enum AttributeType { + STRING = 'string', + NUMBER = 'number', + BOOLEAN = 'boolean', + DATE = 'date', + OBJECT = 'object', + ARRAY = 'array', + IMAGE = 'image' +} export class CredentialAttributeDto { + @ApiProperty({ description: 'Unique key for this attribute (e.g., full_name, org.iso.23220.photoID.1.birth_date)' }) + @IsString() + key: string; + @ApiProperty({ required: false, description: 'Whether the attribute is mandatory' }) @IsOptional() @IsBoolean() mandatory?: boolean; // TODO: Check how do we handle claims with only path rpoperty like email, etc. - @ApiProperty({ description: 'Type of the attribute value (string, number, date, etc.)' }) - @IsString() + @ApiProperty({ enum: AttributeType, description: 'Type of the attribute value (string, number, date, etc.)' }) + @IsEnum(AttributeType) value_type: string; + @ApiProperty({ description: 'Whether this attribute should be disclosed (for SD-JWT)' }) + @IsOptional() + @IsBoolean() + disclose?: boolean; + + @ApiProperty({ type: [CredentialAttributeDisplayDto], required: false, description: 'Localized display values' }) + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => CredentialAttributeDisplayDto) + display?: CredentialAttributeDisplayDto[]; + @ApiProperty({ - type: [String], - description: - 'Claims path pointer as per the draft 15 - https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID2.html#name-claims-path-pointer' + description: 'Nested attributes if type is object or array', + required: false, + type: () => [CredentialAttributeDto] }) - @IsArray() - @IsString({ each: true }) - path: string[]; - - @ApiProperty({ type: [DisplayDto], required: false, description: 'Localized display values' }) @IsOptional() + @IsArray() @ValidateNested({ each: true }) - @Type(() => DisplayDto) - display?: DisplayDto[]; + @Type(() => CredentialAttributeDto) + children?: CredentialAttributeDto[]; } - class LogoDto { @ApiPropertyOptional({ example: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg' @@ -84,6 +144,23 @@ class CredentialDisplayDto { @ValidateNested() @Type(() => LogoDto) logo?: LogoDto; + + @ApiPropertyOptional({ example: '#12107c' }) + @IsString() + @IsOptional() + background_color?: string; + + @ApiPropertyOptional({ example: '#FFFFFF' }) + @IsString() + @IsOptional() + text_color?: string; + + @ApiPropertyOptional({ example: { uri: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/ABC-2021-LOGO.svg' } }) + @IsObject() + @IsOptional() + background_image?: { + uri: string; + }; } export class AppearanceDto { @@ -115,7 +192,55 @@ export class AppearanceDto { display: CredentialDisplayDto[]; } -@ApiExtraModels(CredentialAttributeDto) +export class MdocNamespaceDto { + @ApiProperty({ description: 'Namespace key (e.g., org.iso.23220.photoID.1)' }) + @IsString() + namespace: string; + + @ApiProperty({ type: () => [CredentialAttributeDto] }) + @IsArray() + @ArrayNotEmpty() + @ValidateNested({ each: true }) + @Type(() => CredentialAttributeDto) + attributes: CredentialAttributeDto[]; +} +export class MdocTemplateDto { + @ApiProperty({ + description: 'Document type (required when format is "mso_mdoc"; must NOT be provided when format is "vc+sd-jwt")', + example: 'org.iso.23220.photoID.1' + }) + //@ValidateIf((o: CreateCredentialTemplateDto) => 'mso_mdoc' === o.format) + @IsString() + doctype: string; + + @ApiProperty({ type: () => [MdocNamespaceDto] }) + @IsArray() + @ArrayNotEmpty() + @ValidateNested({ each: true }) + @Type(() => MdocNamespaceDto) + namespaces: MdocNamespaceDto[]; +} + +export class SdJwtTemplateDto { + @ApiProperty({ + description: + 'Verifiable Credential Type (required when format is "vc+sd-jwt"; must NOT be provided when format is "mso_mdoc")', + example: 'BirthCertificateCredential-sdjwt' + }) + // @ValidateIf((o: CreateCredentialTemplateDto) => 'vc+sd-jwt' === o.format) + @IsString() + vct: string; + + @ApiProperty({ + type: 'array', + items: { $ref: getSchemaPath(CredentialAttributeDto) }, + description: 'Attributes included in the credential template' + }) + @IsArray() + attributes: CredentialAttributeDto[]; +} + +@ApiExtraModels(CredentialAttributeDto, SdJwtTemplateDto, MdocTemplateDto) export class CreateCredentialTemplateDto { @ApiProperty({ description: 'Template name' }) @IsString() @@ -134,47 +259,37 @@ export class CreateCredentialTemplateDto { @IsEnum(SignerOption) signerOption!: SignerOption; - @ApiProperty({ enum: ['mso_mdoc', 'vc+sd-jwt'], description: 'Credential format type' }) - @IsEnum(['mso_mdoc', 'vc+sd-jwt']) - format: 'mso_mdoc' | 'vc+sd-jwt'; - - @ApiPropertyOptional({ - description: 'Document type (required when format is "mso_mdoc"; must NOT be provided when format is "vc+sd-jwt")', - example: 'org.iso.23220.photoID.1' - }) - @ValidateIf((o: CreateCredentialTemplateDto) => 'mso_mdoc' === o.format) - @IsString() - doctype?: string; + @ApiProperty({ enum: CredentialFormat, description: 'Credential format type' }) + @IsEnum(CredentialFormat) + format: CredentialFormat; - @ValidateIf((o: CreateCredentialTemplateDto) => 'vc+sd-jwt' === o.format) + @ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.SdJwtVc === o.format) @IsEmpty({ message: 'doctype must not be provided when format is "vc+sd-jwt"' }) readonly _doctypeAbsentGuard?: unknown; - @ApiPropertyOptional({ - description: - 'Verifiable Credential Type (required when format is "vc+sd-jwt"; must NOT be provided when format is "mso_mdoc")', - example: 'BirthCertificateCredential-sdjwt' - }) - @ValidateIf((o: CreateCredentialTemplateDto) => 'vc+sd-jwt' === o.format) - @IsString() - vct?: string; - - @ValidateIf((o: CreateCredentialTemplateDto) => 'mso_mdoc' === o.format) + @ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.Mdoc === o.format) @IsEmpty({ message: 'vct must not be provided when format is "mso_mdoc"' }) readonly _vctAbsentGuard?: unknown; + @ApiProperty({ + type: Object, + oneOf: [{ $ref: getSchemaPath(SdJwtTemplateDto) }, { $ref: getSchemaPath(MdocTemplateDto) }], + description: 'Credential template definition (depends on credentialFormat)' + }) + @ValidateNested() + @Type(({ object }) => { + if (object.format === CredentialFormat.Mdoc) { + return MdocTemplateDto; + } else if (object.format === CredentialFormat.SdJwtVc) { + return SdJwtTemplateDto; + } + }) + template: SdJwtTemplateDto | MdocTemplateDto; + @ApiProperty({ default: false, description: 'Indicates whether credentials can be revoked' }) @IsBoolean() canBeRevoked = false; - @ApiProperty({ - type: 'array', - items: { $ref: getSchemaPath(CredentialAttributeDto) }, - description: 'Attributes included in the credential template' - }) - @IsArray() - attributes: CredentialAttributeDto[]; - @ApiProperty({ type: Object, required: false, diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts index c52ad5136..e16ddcb05 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -1,43 +1,8 @@ /* eslint-disable camelcase */ -import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { - IsString, - IsOptional, - IsBoolean, - IsArray, - ValidateNested, - IsUrl, - IsNotEmpty, - IsDefined, - IsInt -} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, IsOptional, IsArray, ValidateNested, IsUrl, IsInt } from 'class-validator'; import { Type } from 'class-transformer'; -export class ClaimDto { - @ApiProperty({ - description: 'The path for nested claims', - example: ['address', 'street_number'], - type: [String] - }) - @Type(() => String) - @IsArray() - path: string[]; - - @ApiProperty({ - description: 'The display label for the claim', - example: 'Email Address' - }) - @IsString() - label: string; - - @ApiProperty({ - description: 'Whether this claim is required for issuance', - example: true - }) - @IsBoolean() - required: boolean; -} - export class LogoDto { @ApiProperty({ description: 'URI pointing to the logo image', @@ -54,7 +19,7 @@ export class LogoDto { alt_text: string; } -export class DisplayDto { +export class IssuerDisplayDto { @ApiProperty({ description: 'The locale for display text', example: 'en-US' @@ -69,14 +34,6 @@ export class DisplayDto { @IsString({ message: 'Error from DisplayDto -> name' }) name: string; - @ApiPropertyOptional({ - description: 'A short description for the credential/claim', - example: 'Digital credential issued to enrolled students' - }) - @IsOptional() - @IsString() - description?: string; - @ApiPropertyOptional({ description: 'Logo display information for the issuer', type: LogoDto @@ -87,68 +44,68 @@ export class DisplayDto { } // TODO: Check where it is used, coz no reference found -@ApiExtraModels(ClaimDto) -export class CredentialConfigurationDto { - @ApiProperty({ - description: 'The format of the credential', - example: 'jwt_vc_json' - }) - @IsString() - @IsDefined({ message: 'format field is required' }) - @IsNotEmpty({ message: 'format property is required' }) - format: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - vct?: string; +// @ApiExtraModels(ClaimDto) +// export class CredentialConfigurationDto { +// @ApiProperty({ +// description: 'The format of the credential', +// example: 'jwt_vc_json' +// }) +// @IsString() +// @IsDefined({ message: 'format field is required' }) +// @IsNotEmpty({ message: 'format property is required' }) +// format: string; - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - doctype?: string; +// @ApiProperty({ required: false }) +// @IsOptional() +// @IsString() +// vct?: string; - @ApiProperty() - @IsString() - scope: string; +// @ApiProperty({ required: false }) +// @IsOptional() +// @IsString() +// doctype?: string; - @ApiProperty({ - description: 'List of claims supported in this credential', - type: [ClaimDto] - }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => ClaimDto) - claims: ClaimDto[]; - // @ApiProperty({ - // description: 'Claims supported by this credential', - // type: 'object', - // additionalProperties: { $ref: getSchemaPath(ClaimDto) } - // }) - // @IsObject() - // @ValidateNested({ each: true }) - // @Transform(({ value }) => - // Object.fromEntries(Object.entries(value || {}).map(([k, v]) => [k, plainToInstance(ClaimDto, v)])) - // ) - // claims: Record; +// @ApiProperty() +// @IsString() +// scope: string; - @ApiProperty({ type: [String] }) - @IsArray() - credential_signing_alg_values_supported: string[]; +// @ApiProperty({ +// description: 'List of claims supported in this credential', +// type: [ClaimDto] +// }) +// @IsArray() +// @ValidateNested({ each: true }) +// @Type(() => ClaimDto) +// claims: ClaimDto[]; +// // @ApiProperty({ +// // description: 'Claims supported by this credential', +// // type: 'object', +// // additionalProperties: { $ref: getSchemaPath(ClaimDto) } +// // }) +// // @IsObject() +// // @ValidateNested({ each: true }) +// // @Transform(({ value }) => +// // Object.fromEntries(Object.entries(value || {}).map(([k, v]) => [k, plainToInstance(ClaimDto, v)])) +// // ) +// // claims: Record; + +// @ApiProperty({ type: [String] }) +// @IsArray() +// credential_signing_alg_values_supported: string[]; - @ApiProperty({ type: [String] }) - @IsArray() - cryptographic_binding_methods_supported: string[]; +// @ApiProperty({ type: [String] }) +// @IsArray() +// cryptographic_binding_methods_supported: string[]; - @ApiProperty({ - description: 'Localized display information for the credential', - type: [DisplayDto] - }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => DisplayDto) - display: DisplayDto[]; -} +// @ApiProperty({ +// description: 'Localized display information for the credential', +// type: [DisplayDto] +// }) +// @IsArray() +// @ValidateNested({ each: true }) +// @Type(() => DisplayDto) +// display: DisplayDto[]; +// } // export class AuthorizationServerConfigDto { // @ApiProperty({ @@ -239,12 +196,12 @@ export class IssuerCreationDto { @ApiProperty({ description: 'Localized display information for the credential', - type: [DisplayDto] + type: [IssuerDisplayDto] }) @IsArray() @ValidateNested({ each: true }) - @Type(() => DisplayDto) - display: DisplayDto[]; + @Type(() => IssuerDisplayDto) + display: IssuerDisplayDto[]; @ApiProperty({ example: 'https://auth.example.org', description: 'Authorization URL' }) @IsUrl({ require_tld: false }) @@ -265,12 +222,12 @@ export class IssuerUpdationDto { @ApiProperty({ description: 'Localized display information for the credential', - type: [DisplayDto] + type: [IssuerDisplayDto] }) @IsArray() @ValidateNested({ each: true }) - @Type(() => DisplayDto) - display: DisplayDto[]; + @Type(() => IssuerDisplayDto) + display: IssuerDisplayDto[]; @ApiProperty({ description: 'batchCredentialIssuanceSize', diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts index 9ecefedfb..eb5a59ad9 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts @@ -1,4 +1,5 @@ import { organisation } from '@prisma/client'; +import { Claim } from './oid4vc-template.interfaces'; export interface OrgAgent { organisation: organisation; @@ -17,12 +18,6 @@ export interface OrgAgent { tenantId: string; } -export interface Claim { - path: string[]; - label?: string; - required?: boolean; -} - export interface Logo { uri: string; alt_text: string; @@ -114,17 +109,6 @@ export interface TagMap { [key: string]: string; } -export interface ClaimDisplay { - name: string; - locale: string; - description?: string; -} - -export interface ClaimDefinition { - value_type: string; - mandatory: boolean; - display: ClaimDisplay[]; -} export interface Logo { uri: string; alt_text: string; diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts index 7b36b8f18..0e5c32b0b 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -1,11 +1,23 @@ import { Prisma, SignerOption } from '@prisma/client'; -import { Display } from './oid4vc-issuance.interfaces'; import { CredentialFormat } from '@credebl/enum/enum'; -export interface CredentialAttribute { - mandatory?: boolean; - value_type: string; - display?: Display[]; +// export interface CredentialAttribute { +// mandatory?: boolean; +// value_type: string; +// display?: Display[]; +// } + +export interface SdJwtTemplate { + vct: string; + attributes: CredentialAttribute[]; +} + +export interface MdocTemplate { + doctype: string; + namespaces: { + namespace: string; + attributes: CredentialAttribute[]; + }[]; } export interface CreateCredentialTemplate { @@ -13,13 +25,34 @@ export interface CreateCredentialTemplate { description?: string; signerOption?: SignerOption; //SignerOption; format: CredentialFormat; - issuer: string; canBeRevoked: boolean; - attributes: Prisma.JsonValue; + // attributes: Prisma.JsonValue; appearance?: Prisma.JsonValue; issuerId: string; - vct?: string; - doctype?: string; + // vct?: string; + // doctype?: string; + + template: SdJwtTemplate | MdocTemplate; } export interface UpdateCredentialTemplate extends Partial {} + +export interface ClaimDisplay { + name: string; + locale?: string; +} + +export interface Claim { + path: string[]; + display?: ClaimDisplay[]; + mandatory?: boolean; +} + +export interface CredentialAttribute { + key: string; + mandatory?: boolean; + value_type: string; + disclose?: boolean; + children?: CredentialAttribute[]; + display?: ClaimDisplay[]; +} diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index 1e9bfda49..597b2fd06 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -3,14 +3,24 @@ 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, + CredentialAttribute, + MdocTemplate, + SdJwtTemplate +} from 'apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces'; +import { CredentialFormat } from '@credebl/enum/enum'; type AttributeDisplay = { name: string; locale: string }; + +//TODO: Fix this eslint issue +// eslint-disable-next-line @typescript-eslint/no-unused-vars type AttributeDef = { display?: AttributeDisplay[]; mandatory?: boolean; value_type: 'string' | 'date' | 'number' | 'boolean' | string; }; -type AttributesMap = Record; +// type AttributesMap = Record; type CredentialDisplayItem = { logo?: { uri: string; alt_text?: string }; @@ -22,12 +32,12 @@ type Appearance = { display: CredentialDisplayItem[]; }; -type Claim = { - mandatory?: boolean; - // value_type: string; - path: string[]; - display?: AttributeDisplay[]; -}; +// type Claim = { +// mandatory?: boolean; +// // value_type: string; +// path: string[]; +// display?: AttributeDisplay[]; +// }; type CredentialConfig = { format: string; @@ -63,23 +73,39 @@ function coerceJsonObject(v: Prisma.JsonValue): T | 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); -} +// 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: Prisma.JsonValue; // JsonValue from DB + attributes: SdJwtTemplate | MdocTemplate; // JsonValue from DB appearance: Prisma.JsonValue; // JsonValue from DB issuerId: string; createdAt?: Date | string; @@ -90,110 +116,112 @@ type TemplateRowPrisma = { * Build agent payload from Prisma rows (attributes/appearance are Prisma.JsonValue). * Safely coerces JSON and then builds the same structure as Builder #2. */ -export function buildCredentialConfigurationsSupported( - templateRows: TemplateRowPrisma[], - options?: { - vct?: string; - doctype?: string; - scopeVct?: string; - keyResolver?: (templateRow: TemplateRowPrisma) => string; - format?: string; - } -): Record { - const defaultFormat = options?.format ?? 'vc+sd-jwt'; - const credentialConfigMap: Record = {}; - - for (const templateRow of templateRows) { - // Extract and validate attributes (claims) and appearance (display configuration) - const attributesJson = templateRow.attributes; - const appearanceJson = coerceJsonObject(templateRow.appearance); - - if (!isAttributesMap(attributesJson)) { - throw new Error(`Template ${templateRow.id}: invalid attributes JSON`); - } - - if (!isAppearance(appearanceJson)) { - throw new Error(`Template ${templateRow.id}: invalid appearance JSON (missing display array)`); - } - - // Determine credential format (either sd-jwt or mso_mdoc) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const templateFormat: string = (templateRow as any).format ?? defaultFormat; - const isMdocFormat = 'mso_mdoc' === templateFormat; - const formatSuffix = isMdocFormat ? 'mdoc' : 'sdjwt'; - - // Determine the unique key for this credential configuration - const configKey = - 'function' === typeof options?.keyResolver - ? options.keyResolver(templateRow) - : `${templateRow.name}-${formatSuffix}`; - - // Resolve Doctype and VCT based on format type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let resolvedDoctype: string | undefined = options?.doctype ?? (templateRow as any).doctype; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resolvedVct: string = options?.vct ?? (templateRow as any).vct ?? templateRow.name; - - if (isMdocFormat && !resolvedDoctype) { - resolvedDoctype = templateRow.name; // fallback to template name - } - - // Construct OIDC4VC scope - const scopeBaseValue = options?.scopeVct ?? (isMdocFormat ? resolvedDoctype : resolvedVct); - const credentialScope = `openid4vc:credential:${scopeBaseValue}-${formatSuffix}`; - - // Convert each attribute into a claim definition (map shape) - const claimsObject: Record = {}; - for (const [claimName, attributeDefinition] of Object.entries(attributesJson)) { - const parsedAttribute = attributeDefinition as AttributeDef; - - claimsObject[claimName] = { - path: [claimName], - mandatory: parsedAttribute.mandatory ?? false, - display: Array.isArray(parsedAttribute.display) - ? parsedAttribute.display.map((displayItem) => ({ - name: displayItem.name, - locale: displayItem.locale - })) - : undefined, - value_type: parsedAttribute.value_type - }; - } - - // Prepare the display configuration - const displayConfigurations = - (appearanceJson as 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 - })) ?? []; - - // Assemble final credential configuration - credentialConfigMap[configKey] = { - format: templateFormat, - scope: credentialScope, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - claims: Object.entries(claimsObject).map(([claimName, claimDef]) => ({ - path: claimDef.path, - mandatory: claimDef.mandatory, - display: claimDef.display - // you can optionally expose claimDef.value_type here if your API schema allows - })), - credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], - cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], - display: displayConfigurations, - ...(isMdocFormat ? { doctype: resolvedDoctype as string } : { vct: resolvedVct }) - }; - } - - return credentialConfigMap; // ✅ Return flat map, not nested object -} +// export function buildCredentialConfigurationsSupported( +// templateRows: TemplateRowPrisma[], +// options?: { +// vct?: string; +// doctype?: string; +// scopeVct?: string; +// keyResolver?: (templateRow: TemplateRowPrisma) => string; +// format?: string; +// } +// ): Record { +// const defaultFormat = options?.format ?? 'vc+sd-jwt'; +// const credentialConfigMap: Record = {}; + +// for (const templateRow of templateRows) { +// // Extract and validate attributes (claims) and appearance (display configuration) +// const attributesJson = templateRow.attributes; +// const appearanceJson = coerceJsonObject(templateRow.appearance); + +// if (!isAttributesMap(attributesJson)) { +// throw new Error(`Template ${templateRow.id}: invalid attributes JSON`); +// } + +// if (!isAppearance(appearanceJson)) { +// throw new Error(`Template ${templateRow.id}: invalid appearance JSON (missing display array)`); +// } + +// // Determine credential format (either sd-jwt or mso_mdoc) +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// const templateFormat: string = (templateRow as any).format ?? defaultFormat; +// const isMdocFormat = 'mso_mdoc' === templateFormat; +// const formatSuffix = isMdocFormat ? 'mdoc' : 'sdjwt'; + +// // Determine the unique key for this credential configuration +// const configKey = +// 'function' === typeof options?.keyResolver +// ? options.keyResolver(templateRow) +// : `${templateRow.name}-${formatSuffix}`; + +// // Resolve Doctype and VCT based on format type +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// let resolvedDoctype: string | undefined = options?.doctype ?? (templateRow as any).doctype; +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// const resolvedVct: string = options?.vct ?? (templateRow as any).vct ?? templateRow.name; + +// if (isMdocFormat && !resolvedDoctype) { +// resolvedDoctype = templateRow.name; // fallback to template name +// } + +// // Construct OIDC4VC scope +// const scopeBaseValue = options?.scopeVct ?? (isMdocFormat ? resolvedDoctype : resolvedVct); +// const credentialScope = `openid4vc:credential:${scopeBaseValue}-${formatSuffix}`; + +// // Convert each attribute into a claim definition (map shape) +// const claimsObject: Record = {}; +// for (const [claimName, attributeDefinition] of Object.entries(attributesJson)) { +// console.log(`claimName, attributeDefinition`, claimName, attributeDefinition); +// console.log(`attributesJson`, attributesJson); +// const parsedAttribute = attributeDefinition as AttributeDef; + +// claimsObject[claimName] = { +// path: [claimName], +// mandatory: parsedAttribute.mandatory ?? false, +// display: Array.isArray(parsedAttribute.display) +// ? parsedAttribute.display.map((displayItem) => ({ +// name: displayItem.name, +// locale: displayItem.locale +// })) +// : undefined, +// value_type: parsedAttribute.value_type +// }; +// } + +// // Prepare the display configuration +// const displayConfigurations = +// (appearanceJson as 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 +// })) ?? []; + +// // Assemble final credential configuration +// credentialConfigMap[configKey] = { +// format: templateFormat, +// scope: credentialScope, +// // eslint-disable-next-line @typescript-eslint/no-unused-vars +// claims: Object.entries(claimsObject).map(([claimName, claimDef]) => ({ +// path: claimDef.path, +// mandatory: claimDef.mandatory, +// display: claimDef.display +// // you can optionally expose claimDef.value_type here if your API schema allows +// })), +// credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], +// cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], +// display: displayConfigurations, +// ...(isMdocFormat ? { doctype: resolvedDoctype as string } : { vct: resolvedVct }) +// }; +// } + +// return credentialConfigMap; // ✅ Return flat map, not nested object +// } /** * Helper — Optional @@ -270,7 +298,7 @@ export function buildIssuerPayload( return { display, dpopSigningAlgValuesSupported: opts?.dpopAlgs ?? [...ISSUER_DPOP_ALGS_DEFAULT], - credentialConfigurationsSupported: credentialConfigurations ?? [], + credentialConfigurationsSupported: credentialConfigurations.credentialConfigurationsSupported ?? [], batchCredentialIssuance: { batchSize: oidcIssuer?.batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault } @@ -301,3 +329,151 @@ export function encodeIssuerPublicId(publicIssuerId: string): string { } return encodeURIComponent(publicIssuerId.trim()); } + +///--------------------------------------------------------- + +function buildClaimsFromAttributes(attributes: CredentialAttribute[], parentPath: string[] = []): Claim[] { + const claims: Claim[] = []; + + for (const attr of attributes) { + const currentPath = [...parentPath, attr.key]; + + // 1️⃣ Add the parent attribute itself if it has display or mandatory metadata + if ((attr.display && 0 < attr.display.length) || attr.mandatory) { + const parentClaim: Claim = { path: currentPath }; + + if (attr.display?.length) { + parentClaim.display = attr.display.map((d) => ({ + name: d.name, + locale: d.locale + })); + } + + if (attr.mandatory) { + parentClaim.mandatory = true; + } + + claims.push(parentClaim); + } + + // 2️⃣ If this attribute has nested children, recurse into them + if (attr.children && 0 < attr.children.length) { + claims.push(...buildClaimsFromAttributes(attr.children, currentPath)); + } + } + return claims; +} + +//TODO: Fix this eslint issue +// 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 + const configKey = `${name}-${formatSuffix}`; + const credentialScope = `openid4vc:${template.vct}-${formatSuffix}`; + + const claims = buildClaimsFromAttributes(template.attributes); + + return { + [configKey]: { + format: CredentialFormat.SdJwtVc, + scope: credentialScope, + vct: template.vct, + credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], + cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], + // proof_types_supported: { + // jwt: { + // proof_signing_alg_values_supported: ['ES256'] + // } + // }, + claims + } + }; +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function buildMdocCredentialConfig(name: string, template: MdocTemplate) { + const claims: Claim[] = []; + + const formatSuffix = 'mdoc'; + + // Determine the unique key for this credential configuration + const configKey = `${name}-${formatSuffix}`; + const credentialScope = `openid4vc:${template.doctype}-${formatSuffix}`; + + for (const ns of template.namespaces) { + claims.push(...buildClaimsFromAttributes(ns.attributes, [ns.namespace])); + } + + return { + [configKey]: { + format: CredentialFormat.Mdoc, + scope: credentialScope, + doctype: template.doctype, + credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], + cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], + claims + } + }; +} + +//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.Mdoc: + return buildMdocCredentialConfig(name, template as MdocTemplate); + default: + throw new Error(`Unsupported credential format: ${format}`); + } +} + +/** + * 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 buildCredentialConfigurationsSupportedNew(templateRows: any): Record { + const credentialConfigMap: Record = {}; + + for (const templateRow of templateRows) { + const { format } = templateRow; + const templateToBuild = templateRow.attributes; + + const credentialConfig = buildCredentialConfig( + templateRow.name, + templateToBuild, + format === CredentialFormat.Mdoc ? CredentialFormat.Mdoc : CredentialFormat.SdJwtVc + ); + + const appearanceJson = coerceJsonObject(templateRow.appearance); + + // Prepare the display configuration + const displayConfigurations = + (appearanceJson as 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 + })) ?? []; + + // eslint-disable-next-line prefer-destructuring + const dynamicKey = Object.keys(credentialConfig)[0]; + Object.assign(credentialConfig[dynamicKey], { + display: displayConfigurations + }); + + Object.assign(credentialConfigMap, credentialConfig); + } + + return credentialConfigMap; // ✅ Return flat map, not nested object +} diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 3cae5052c..21ec91436 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -23,7 +23,7 @@ import { ResponseMessages } from '@credebl/common/response-messages'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { map } from 'rxjs'; import { getAgentUrl } from '@credebl/common/common.utils'; -import { credential_templates, oidc_issuer, SignerOption, user } from '@prisma/client'; +import { credential_templates, oidc_issuer, Prisma, SignerOption, user } from '@prisma/client'; import { IAgentOIDCIssuerCreate, IssuerCreation, @@ -40,7 +40,7 @@ import { dpopSigningAlgValuesSupported } from '../constant/issuance'; import { - buildCredentialConfigurationsSupported, + buildCredentialConfigurationsSupportedNew, buildIssuerPayload, encodeIssuerPublicId, extractTemplateIds, @@ -58,6 +58,7 @@ import { CredentialOfferPayload } from '../libs/helpers/credential-sessions.builder'; import { x5cKeyType } from '@credebl/enum/enum'; +import { instanceToPlain, plainToInstance } from 'class-transformer'; type CredentialDisplayItem = { logo?: { uri: string; alt_text?: string }; @@ -285,14 +286,14 @@ export class Oid4vcIssuanceService { } async createTemplate( - CredentialTemplate: CreateCredentialTemplate, + credentialTemplate: CreateCredentialTemplate, orgId: string, issuerId: string ): Promise { try { //TODO: add revert mechanism if agent call fails - const { name, description, format, canBeRevoked, attributes, appearance, signerOption, vct, doctype } = - CredentialTemplate; + const { name, description, format, canBeRevoked, appearance, signerOption } = credentialTemplate; + const checkNameExist = await this.oid4vcIssuanceRepository.getTemplateByNameForIssuer(name, issuerId); if (0 < checkNameExist.length) { throw new ConflictException(ResponseMessages.oidcTemplate.error.templateNameAlreadyExist); @@ -302,7 +303,7 @@ export class Oid4vcIssuanceService { description, format: format.toString(), canBeRevoked, - attributes, + attributes: instanceToPlain(credentialTemplate.template), appearance: appearance ?? {}, issuerId, signerOption @@ -313,14 +314,14 @@ export class Oid4vcIssuanceService { if (!createdTemplate) { throw new InternalServerErrorException(ResponseMessages.oidcTemplate.error.createFailed); } - let opts = {}; - if (vct) { - opts = { ...opts, vct }; - } - if (doctype) { - opts = { ...opts, doctype }; - } - const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId, opts); + // let opts = {}; + // if (vct) { + // opts = { ...opts, vct }; + // } + // if (doctype) { + // opts = { ...opts, doctype }; + // } + const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); console.log(`service - createTemplate: `, JSON.stringify(issuerTemplateConfig)); const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); if (!agentDetails) { @@ -360,7 +361,7 @@ export class Oid4vcIssuanceService { updateCredentialTemplate.name, issuerId ); - if (0 < checkNameExist.length) { + if (0 < checkNameExist.length && !checkNameExist.some((item) => item.id === templateId)) { throw new ConflictException(ResponseMessages.oidcTemplate.error.templateNameAlreadyExist); } } @@ -368,7 +369,8 @@ export class Oid4vcIssuanceService { ...updateCredentialTemplate, ...(issuerId ? { issuerId } : {}) }; - const { name, description, format, canBeRevoked, attributes, appearance } = normalized; + const { name, description, format, canBeRevoked, appearance } = normalized; + const attributes = instanceToPlain(normalized.template); const payload = { ...(name !== undefined ? { name } : {}), @@ -699,14 +701,17 @@ export class Oid4vcIssuanceService { } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - async buildOidcIssuerConfig(issuerId: string, configMetadata?) { + async buildOidcIssuerConfig(issuerId: string) { try { const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); const templates = await this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); - const credentialConfigurationsSupported = buildCredentialConfigurationsSupported(templates, configMetadata); + console.log(`---------------- emplates, configMetadata`, templates); + const credentialConfigurationsSupported = buildCredentialConfigurationsSupportedNew(templates); + + console.log(`-------------------credentialConfigurationsSupported`, credentialConfigurationsSupported); - return buildIssuerPayload(credentialConfigurationsSupported, issuerDetails); + return buildIssuerPayload({ credentialConfigurationsSupported }, issuerDetails); } catch (error) { this.logger.error(`[buildOidcIssuerPayload] - error: ${JSON.stringify(error)}`); throw new RpcException(error.response ?? error); From a891c95cf9b0b8120d8153807883ec2f118cff53 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Tue, 28 Oct 2025 00:56:59 +0530 Subject: [PATCH 14/43] refactor:updated example for issuer and added logic to fetch issuer details Signed-off-by: Tipu_Singh --- .../oid4vc-issuance/dtos/oid4vc-issuer.dto.ts | 54 +++++++++++++++---- .../src/oid4vc-issuance.repository.ts | 11 ++++ .../src/oid4vc-issuance.service.ts | 6 ++- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts index e16ddcb05..45b360cae 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -179,15 +179,15 @@ export enum AccessTokenSignerKeyType { // @ApiExtraModels(CredentialConfigurationDto) export class IssuerCreationDto { @ApiProperty({ - description: 'Name of the issuer', - example: 'Credebl University' + description: 'Unique identifier of the issuer (usually a short code or DID-based identifier)', + example: 'credebl-university' }) - @IsString({ message: 'issuerId from IssuerCreationDto -> issuerId, must be a string' }) + @IsString({ message: 'issuerId must be a string' }) issuerId: string; @ApiPropertyOptional({ - description: 'Maximum number of credentials that can be issued in a batch', - example: 50, + description: 'Maximum number of credentials that can be issued in a single batch issuance operation', + example: 100, type: Number }) @IsOptional() @@ -195,21 +195,53 @@ export class IssuerCreationDto { batchCredentialIssuanceSize?: number; @ApiProperty({ - description: 'Localized display information for the credential', - type: [IssuerDisplayDto] + description: + 'Localized display information for the issuer — shown in wallet apps or credential metadata display (multi-lingual supported)', + type: [IssuerDisplayDto], + example: [ + { + locale: 'en', + name: 'Credebl University', + description: 'Accredited institution issuing verified student credentials', + logo: { + uri: 'https://university.credebl.io/assets/logo-en.svg', + alt_text: 'Credebl University logo' + } + }, + { + locale: 'de', + name: 'Credebl Universität', + description: 'Akkreditierte Institution für digitale Studentenausweise', + logo: { + uri: 'https://university.credebl.io/assets/logo-de.svg', + alt_text: 'Credebl Universität Logo' + } + } + ] }) @IsArray() @ValidateNested({ each: true }) @Type(() => IssuerDisplayDto) display: IssuerDisplayDto[]; - @ApiProperty({ example: 'https://auth.example.org', description: 'Authorization URL' }) - @IsUrl({ require_tld: false }) + @ApiProperty({ + example: 'https://issuer.credebl.io/oid4vci', + description: 'Base URL of the Authorization Server supporting OID4VC issuance flows' + }) + @IsUrl({ require_tld: false }, { message: 'authorizationServerUrl must be a valid URL' }) authorizationServerUrl: string; @ApiProperty({ - description: 'Configuration of the authorization server', - type: AuthorizationServerConfigDto + description: + 'Additional configuration details for the authorization server (token endpoint, credential endpoint, grant types, etc.)', + type: AuthorizationServerConfigDto, + example: { + issuer: 'https://id.sovio.ae:8443/realms/sovioid', + clientAuthentication: { + clientId: 'issuer-server', + clientSecret: '1qKMWulZpMBzXIdfPO5AEs0xaTaKs1ym' + } + } }) @IsOptional() @ValidateNested() diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts index e2e6a3715..d7651172b 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts @@ -307,6 +307,17 @@ export class Oid4vcIssuanceRepository { } } + async getIssuerDetailsByIssuerId(issuerId: string): Promise { + try { + return await this.prisma.oidc_issuer.findUnique({ + where: { id: issuerId } + }); + } catch (error) { + this.logger.error(`Error in getIssuerDetailsByIssuerId: ${error.message}`); + throw error; + } + } + async updateTemplate(templateId: string, data: Partial): Promise { try { return await this.prisma.credential_templates.update({ diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 21ec91436..973558471 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -524,13 +524,15 @@ export class Oid4vcIssuanceService { } //TODO: Implement x509 support and discuss with team //TODO: add logic to pass the issuer info + const issuerDetailsFromDb = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + const { publicIssuerId, authorizationServerUrl } = issuerDetailsFromDb || {}; const buildOidcCredentialOffer: CredentialOfferPayload = buildCredentialOfferPayload( createOidcCredentialOffer, // getAllOfferTemplates, { - publicId: 'photoIdIssuer', - authorizationServerUrl: 'http://localhost:4002/oid4vci/photoIdIssuer' + publicId: publicIssuerId, + authorizationServerUrl: `${authorizationServerUrl}/oid4vci/${publicIssuerId}` }, signerOptions as any ); From 259455fcbb40e92c485ff3a0eea20cabb06e7332 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Tue, 28 Oct 2025 03:28:01 +0530 Subject: [PATCH 15/43] fix: correction in create offer functions for validations and builder method Signed-off-by: Rinkal Bhojani --- .../oid4vc-issuer-sessions.interfaces.ts | 8 +- .../helpers/credential-sessions.builder.ts | 240 ++++++++++++++++++ .../libs/helpers/issuer.metadata.ts | 112 ++++++-- .../src/oid4vc-issuance.service.ts | 3 +- 4 files changed, 333 insertions(+), 30 deletions(-) diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts index 6fd33ead5..69e8f97d1 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts @@ -30,10 +30,10 @@ export enum AuthenticationType { export type DisclosureFrame = Record>; export interface CredentialPayload { - full_name?: string; - birth_date?: string; // YYYY-MM-DD if present - birth_place?: string; - parent_names?: string; + validityInfo: { + validFrom: Date; + validUntil: Date; + }; [key: string]: unknown; // extensible for mDoc or other formats } diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 2824940bd..2abc31e68 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -3,6 +3,12 @@ import { Prisma, credential_templates } from '@prisma/client'; import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; import { CredentialFormat } from '@credebl/enum/enum'; +import { + CredentialAttribute, + MdocTemplate, + SdJwtTemplate +} from 'apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces'; +import { UnprocessableEntityException } from '@nestjs/common'; /* ============================================================================ Domain Types @@ -494,3 +500,237 @@ export function buildCredentialOfferUrl(baseUrl: string, getAllCredentialOffer: // Append query string if any params exist return 0 < criteriaParams.length ? `${baseUrl}?${criteriaParams.join('&')}` : baseUrl; } + +export function validatePayloadAgainstTemplate(template: any, payload: any): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + const validateAttributes = (attributes: CredentialAttribute[], data: any, path = '') => { + for (const attr of attributes) { + const currentPath = path ? `${path}.${attr.key}` : attr.key; + const value = data?.[attr.key]; + + // Check for missing mandatory value + const isEmpty = + value === undefined || + null === value || + ('string' === typeof value && '' === value.trim()) || + ('object' === typeof value && !Array.isArray(value) && 0 === Object.keys(value).length); + + if (attr.mandatory && isEmpty) { + errors.push(`Missing mandatory attribute: ${currentPath}`); + } + + // Recurse for nested attributes + if (attr.children && 'object' === typeof value && null !== value) { + validateAttributes(attr.children, value, currentPath); + } + } + }; + + if (CredentialFormat.SdJwtVc === template.format) { + validateAttributes((template.attributes as SdJwtTemplate).attributes ?? [], payload); + } else if (CredentialFormat.Mdoc === template.format) { + const namespaces = payload?.namespaces; + if (!namespaces) { + errors.push('Missing namespaces object in mdoc payload.'); + } else { + const templateNamespaces = (template.attributes as MdocTemplate).namespaces; + for (const ns of templateNamespaces ?? []) { + const nsData = namespaces[ns.namespace]; + if (!nsData) { + errors.push(`Missing namespace: ${ns.namespace}`); + continue; + } + validateAttributes(ns.attributes, nsData, ns.namespace); + } + } + } + + return { valid: 0 === errors.length, errors }; +} + +function buildDisclosureFrameFromTemplate(template: { attributes: CredentialAttribute[] }) { + const disclosureFrame: DisclosureFrame = {}; + + const buildFrame = (attributes: CredentialAttribute[]) => { + const frame: Record = {}; + + for (const attr of attributes) { + if (attr.children?.length) { + // Handle nested attributes recursively + const subFrame = buildFrame(attr.children); + // Include parent only if disclose is true or it has children with disclosure + if (attr.disclose || 0 < Object.keys(subFrame).length) { + frame[attr.key] = subFrame; + } + } else if (attr.disclose !== undefined) { + frame[attr.key] = Boolean(attr.disclose); + } + } + + return frame; + }; + + Object.assign(disclosureFrame, buildFrame(template.attributes)); + + return disclosureFrame; +} + +function buildSdJwtCredentialNew( + credentialRequest: CredentialRequestDtoLike, + templateRecord: any, + signerOptions?: SignerOption[] +): BuiltCredential { + // For SD-JWT format we expect payload to be a flat map of claims (no namespaces) + const payloadCopy = { ...(credentialRequest.payload as Record) }; + + // // strip vct if present per requirement + // delete payloadCopy.vct; + + const sdJwtTemplate = templateRecord.attributes as SdJwtTemplate; + payloadCopy.vct = sdJwtTemplate.vct; + + const apiFormat = mapDbFormatToApiFormat(templateRecord.format); + const idSuffix = formatSuffix(apiFormat); + const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + const disclosureFrame = buildDisclosureFrameFromTemplate({ attributes: sdJwtTemplate.attributes }); + + return { + credentialSupportedId, + signerOptions: signerOptions ? signerOptions[0] : undefined, + format: apiFormat, + payload: payloadCopy, + ...(disclosureFrame ? { disclosureFrame } : {}) + }; +} + +/** Build an MSO mdoc credential object + * - For mdocs we expect the payload to include a `namespaces` map (draft-15 style) + */ +function buildMdocCredentialNew( + credentialRequest: CredentialRequestDtoLike, + templateRecord: any, + signerOptions?: SignerOption[] +): BuiltCredential { + const incomingPayload = { ...(credentialRequest.payload as Record) }; + + // // If caller provided already-namespaced payload, keep it; otherwise build a namespaces map + // const workingPayload = { ...incomingPayload }; + // if (!workingPayload.namespaces) { + // const namespacesMap: Record> = {}; + // // collect claims that match attribute names into the chosen namespace + // for (const claimName of Object.keys(normalizedAttributes)) { + // if (Object.prototype.hasOwnProperty.call(incomingPayload, claimName)) { + // namespacesMap[defaultNamespace] = namespacesMap[defaultNamespace] ?? {}; + // namespacesMap[defaultNamespace][claimName] = (incomingPayload as any)[claimName]; + // // remove original flattened claim to avoid duplication + // delete (workingPayload as any)[claimName]; + // } + // } + // if (0 < Object.keys(namespacesMap).length) { + // (workingPayload as any).namespaces = namespacesMap; + // } + // } else { + // // ensure namespaces is a plain object + // if (!isPlainRecord((workingPayload as any).namespaces)) { + // throw new Error(`Invalid mdoc payload: 'namespaces' must be an object`); + // } + // } + + const apiFormat = mapDbFormatToApiFormat(templateRecord.format); + const idSuffix = formatSuffix(apiFormat); + const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + + return { + credentialSupportedId, + signerOptions: signerOptions ? signerOptions[0] : undefined, + format: apiFormat, + payload: incomingPayload, + ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) + }; +} + +export function buildCredentialOfferPayloadNew( + dto: CreateOidcCredentialOfferDtoLike, + templates: credential_templates[], + issuerDetails?: { + publicId: string; + authorizationServerUrl?: string; + }, + signerOptions?: SignerOption[] +): CredentialOfferPayload { + // Index templates by id + const templatesById = new Map(templates.map((template) => [template.id, template])); + + // Validate template ids + const missingTemplateIds = dto.credentials.map((c) => c.templateId).filter((id) => !templatesById.has(id)); + if (missingTemplateIds.length) { + throw new Error(`Unknown template ids: ${missingTemplateIds.join(', ')}`); + } + + // Build each credential using the template's format + const builtCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { + const templateRecord = templatesById.get(credentialRequest.templateId)!; + + const validationError = validatePayloadAgainstTemplate(templateRecord, credentialRequest.payload); + if (!validationError.valid) { + throw new UnprocessableEntityException(`${validationError.errors.join(', ')}`); + } + + const templateFormat = (templateRecord as any).format ?? 'vc+sd-jwt'; + const apiFormat = mapDbFormatToApiFormat(templateFormat); + + if (apiFormat === CredentialFormat.SdJwtVc) { + return buildSdJwtCredentialNew(credentialRequest, templateRecord, signerOptions); + } + if (apiFormat === CredentialFormat.Mdoc) { + return buildMdocCredentialNew(credentialRequest, templateRecord, signerOptions); + } + throw new Error(`Unsupported template format for ${templateFormat}`); + }); + + // Base envelope: allow explicit publicIssuerId from DTO or fallback to issuerDetails.publicId + const publicIssuerIdFromDto = dto.publicIssuerId; + const publicIssuerIdFromIssuerDetails = issuerDetails?.publicId; + const finalPublicIssuerId = publicIssuerIdFromDto ?? publicIssuerIdFromIssuerDetails; + + const baseEnvelope: BuiltCredentialOfferBase = { + credentials: builtCredentials, + ...(finalPublicIssuerId ? { publicIssuerId: finalPublicIssuerId } : {}) + }; + + // Determine which authorization flow to return: + // Priority: + // 1) If issuerDetails.authorizationServerUrl is provided, return preAuthorizedCodeFlowConfig using DEFAULT_TXCODE + // 2) Else fall back to flows present in DTO (still enforce XOR) + const overrideAuthorizationServerUrl = issuerDetails?.authorizationServerUrl; + if (overrideAuthorizationServerUrl) { + if ('string' !== typeof overrideAuthorizationServerUrl || '' === overrideAuthorizationServerUrl.trim()) { + throw new Error('issuerDetails.authorizationServerUrl must be a non-empty string when provided'); + } + return { + ...baseEnvelope, + preAuthorizedCodeFlowConfig: { + txCode: DEFAULT_TXCODE, + authorizationServerUrl: overrideAuthorizationServerUrl + } + }; + } + + // No override provided — use what DTO carries (must be XOR) + const hasPreAuthFromDto = Boolean(dto.preAuthorizedCodeFlowConfig); + const hasAuthCodeFromDto = Boolean(dto.authorizationCodeFlowConfig); + if (hasPreAuthFromDto === hasAuthCodeFromDto) { + throw new Error('Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'); + } + if (hasPreAuthFromDto) { + return { + ...baseEnvelope, + preAuthorizedCodeFlowConfig: dto.preAuthorizedCodeFlowConfig! + }; + } + return { + ...baseEnvelope, + authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! + }; +} diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index 597b2fd06..ee4ae452c 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -332,38 +332,98 @@ export function encodeIssuerPublicId(publicIssuerId: string): string { ///--------------------------------------------------------- -function buildClaimsFromAttributes(attributes: CredentialAttribute[], parentPath: string[] = []): Claim[] { - const claims: Claim[] = []; +// function buildClaimsFromAttributesWithPath(attributes: CredentialAttribute[], parentPath: string[] = []): Claim[] { +// const claims: Claim[] = []; - for (const attr of attributes) { - const currentPath = [...parentPath, attr.key]; +// for (const attr of attributes) { +// const currentPath = [...parentPath, attr.key]; + +// // 1️⃣ Add the parent attribute itself if it has display or mandatory metadata +// if ((attr.display && 0 < attr.display.length) || attr.mandatory) { +// const parentClaim: Claim = { path: currentPath }; + +// if (attr.display?.length) { +// parentClaim.display = attr.display.map((d) => ({ +// name: d.name, +// locale: d.locale +// })); +// } + +// if (attr.mandatory) { +// parentClaim.mandatory = true; +// } - // 1️⃣ Add the parent attribute itself if it has display or mandatory metadata - if ((attr.display && 0 < attr.display.length) || attr.mandatory) { - const parentClaim: Claim = { path: currentPath }; +// claims.push(parentClaim); +// } + +// // 2️⃣ If this attribute has nested children, recurse into them +// if (attr.children && 0 < attr.children.length) { +// claims.push(...buildClaimsFromAttributes(attr.children, currentPath)); +// } +// } +// return claims; +// } - if (attr.display?.length) { - parentClaim.display = attr.display.map((d) => ({ - name: d.name, - locale: d.locale - })); - } +/** + * Recursively builds a nested claims object from a list of attributes. + */ +function buildNestedClaims(attributes: CredentialAttribute[]): Record { + const claims: Record = {}; - if (attr.mandatory) { - parentClaim.mandatory = true; - } + for (const attr of attributes) { + const node: Claim = {}; + + // ✅ include display info + if (attr.display?.length) { + node.display = attr.display.map((d) => ({ + name: d.name, + locale: d.locale + })); + } - claims.push(parentClaim); + // ✅ include mandatory flag + if (attr.mandatory) { + node.mandatory = true; } - // 2️⃣ If this attribute has nested children, recurse into them - if (attr.children && 0 < attr.children.length) { - claims.push(...buildClaimsFromAttributes(attr.children, currentPath)); + // ✅ handle nested children recursively + if (attr.children?.length) { + const childClaims = buildNestedClaims(attr.children); + Object.assign(node, childClaims); // merge children into current node } + + claims[attr.key] = node; } + return claims; } +/** + * Builds claims object for both SD-JWT and MDOC credential templates. + */ +//TODO: Remove any type +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function buildClaimsFromTemplate(template: SdJwtTemplate | MdocTemplate): Record { + // ✅ MDOC case — handle namespaces + if ((template as MdocTemplate).namespaces) { + const mdocTemplate = template as MdocTemplate; + + //TODO: Remove any type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const claims: Record = {}; + + for (const ns of mdocTemplate.namespaces) { + claims[ns.namespace] = buildNestedClaims(ns.attributes); + } + + return claims; + } + + // ✅ SD-JWT case — flat attributes + const sdjwtTemplate = template as SdJwtTemplate; + return buildNestedClaims(sdjwtTemplate.attributes); +} + //TODO: Fix this eslint issue // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate) { @@ -373,7 +433,7 @@ export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate const configKey = `${name}-${formatSuffix}`; const credentialScope = `openid4vc:${template.vct}-${formatSuffix}`; - const claims = buildClaimsFromAttributes(template.attributes); + const claims = buildClaimsFromTemplate(template); return { [configKey]: { @@ -394,7 +454,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) { - const claims: Claim[] = []; + //const claims: Claim[] = []; const formatSuffix = 'mdoc'; @@ -402,9 +462,11 @@ export function buildMdocCredentialConfig(name: string, template: MdocTemplate) const configKey = `${name}-${formatSuffix}`; const credentialScope = `openid4vc:${template.doctype}-${formatSuffix}`; - for (const ns of template.namespaces) { - claims.push(...buildClaimsFromAttributes(ns.attributes, [ns.namespace])); - } + const claims = buildClaimsFromTemplate(template); + + // for (const ns of template.namespaces) { + // claims.push(...buildClaimsFromAttributes(ns.attributes, [ns.namespace])); + // } return { [configKey]: { diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 973558471..68b84ef85 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -54,6 +54,7 @@ import { } from '../interfaces/oid4vc-issuer-sessions.interfaces'; import { buildCredentialOfferPayload, + buildCredentialOfferPayloadNew, buildCredentialOfferUrl, CredentialOfferPayload } from '../libs/helpers/credential-sessions.builder'; @@ -526,7 +527,7 @@ export class Oid4vcIssuanceService { //TODO: add logic to pass the issuer info const issuerDetailsFromDb = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); const { publicIssuerId, authorizationServerUrl } = issuerDetailsFromDb || {}; - const buildOidcCredentialOffer: CredentialOfferPayload = buildCredentialOfferPayload( + const buildOidcCredentialOffer: CredentialOfferPayload = buildCredentialOfferPayloadNew( createOidcCredentialOffer, // getAllOfferTemplates, From 9fe57eef4d4389cebb1dfd5fd5d2a9bcf14ff9f7 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Tue, 28 Oct 2025 12:59:49 +0530 Subject: [PATCH 16/43] refactor: create and update template Signed-off-by: Tipu_Singh --- .../oid4vc-issuance/dtos/oid4vc-issuer.dto.ts | 2 +- .../src/oid4vc-issuance.service.ts | 114 +++++++++++++----- libs/common/src/response-messages/index.ts | 6 +- 3 files changed, 89 insertions(+), 33 deletions(-) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts index 45b360cae..cd99e117b 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -225,7 +225,7 @@ export class IssuerCreationDto { display: IssuerDisplayDto[]; @ApiProperty({ - example: 'https://issuer.credebl.io/oid4vci', + example: 'https://issuer.credebl.io', description: 'Base URL of the Authorization Server supporting OID4VC issuance flows' }) @IsUrl({ require_tld: false }, { message: 'authorizationServerUrl must be a valid URL' }) diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 68b84ef85..d4f8f193e 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -322,19 +322,39 @@ export class Oid4vcIssuanceService { // if (doctype) { // opts = { ...opts, doctype }; // } - const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); - console.log(`service - createTemplate: `, JSON.stringify(issuerTemplateConfig)); - const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); - if (!agentDetails) { - throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); - } - const { agentEndPoint } = agentDetails; - const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); - if (!issuerDetails) { - throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + let createTemplateOnAgent; + try { + const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); + console.log(`service - createTemplate: `, JSON.stringify(issuerTemplateConfig)); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint } = agentDetails; + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!issuerDetails) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + } + const url = await getAgentUrl( + agentEndPoint, + CommonConstants.OIDC_ISSUER_TEMPLATE, + issuerDetails.publicIssuerId + ); + createTemplateOnAgent = await this._createOIDCTemplate(issuerTemplateConfig, url, orgId); + } catch (agentError) { + try { + await this.oid4vcIssuanceRepository.deleteTemplate(createdTemplate.id); + this.logger.log(`${ResponseMessages.oidcTemplate.success.deleteTemplate}${createdTemplate.id}`); + throw new RpcException(agentError?.response ?? agentError); + } catch (cleanupError) { + this.logger.error( + `${ResponseMessages.oidcTemplate.error.failedDeleteTemplate}${createdTemplate.id} deleteError=${JSON.stringify( + cleanupError + )} originalAgentError=${JSON.stringify(agentError)}` + ); + throw new RpcException('Template creation failed and cleanup also failed'); + } } - const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_TEMPLATE, issuerDetails.publicIssuerId); - const createTemplateOnAgent = await this._createOIDCTemplate(issuerTemplateConfig, url, orgId); console.log('createTemplateOnAgent::::::::::::::', createTemplateOnAgent); if (!createTemplateOnAgent) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); @@ -385,25 +405,59 @@ export class Oid4vcIssuanceService { const updatedTemplate = await this.oid4vcIssuanceRepository.updateTemplate(templateId, payload); - const templates = await this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); - if (!templates || 0 === templates.length) { - throw new NotFoundException(ResponseMessages.issuance.error.notFound); - } - const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); - const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); - if (!agentDetails) { - throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); - } - const { agentEndPoint } = agentDetails; - const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); - if (!issuerDetails) { - throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); - } - const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_ISSUER_TEMPLATE, issuerDetails.publicIssuerId); + try { + const templates = await this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); + if (!templates || 0 === templates.length) { + throw new NotFoundException(ResponseMessages.issuance.error.notFound); + } + const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); + const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint } = agentDetails; + const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); + if (!issuerDetails) { + throw new NotFoundException(ResponseMessages.oidcTemplate.error.issuerDetailsNotFound); + } + const url = await getAgentUrl( + agentEndPoint, + CommonConstants.OIDC_ISSUER_TEMPLATE, + issuerDetails.publicIssuerId + ); - const createTemplateOnAgent = await this._createOIDCTemplate(issuerTemplateConfig, url, orgId); - if (!createTemplateOnAgent) { - throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + const createTemplateOnAgent = await this._createOIDCTemplate(issuerTemplateConfig, url, orgId); + if (!createTemplateOnAgent) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + } catch (agentError) { + this.logger.error(`[updateTemplate] - error updating template on agent: ${JSON.stringify(agentError)}`); + try { + const rollbackPayload = { + name: template.name, + description: template.description, + format: template.format, + canBeRevoked: template.canBeRevoked, + attributes: template.attributes, + appearance: template.appearance, + issuerId: template.issuerId + }; + await this.oid4vcIssuanceRepository.updateTemplate(templateId, rollbackPayload); + this.logger.log(`Rolled back template ${templateId} to previous state after agent error`); + throw new RpcException(agentError?.response ?? agentError); + } catch (revertError) { + this.logger.error( + `[updateTemplate] - rollback failed for template ${templateId}: ${JSON.stringify(revertError)} originalAgentError=${JSON.stringify( + agentError + )}` + ); + const wrappedError = { + message: 'Template update failed and rollback also failed', + agentError: agentError?.response ?? agentError, + rollbackError: revertError?.response ?? revertError + }; + throw new RpcException(wrappedError); + } } return updatedTemplate; diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 71a08808b..653e6eeca 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -526,7 +526,8 @@ export const ResponseMessages = { update: 'OID4VC template updated successfully.', delete: 'OID4VC template deleted successfully.', fetch: 'OID4VC template(s) fetched successfully.', - getById: 'OID4VC template details fetched successfully.' + getById: 'OID4VC template details fetched successfully.', + deleteTemplate: '[createTemplate] compensating delete succeeded for templateId=${templateId}' }, error: { notFound: 'OID4VC template not found.', @@ -537,7 +538,8 @@ export const ResponseMessages = { issuerDisplayNotFound: 'Issuer display not found.', issuerDetailsNotFound: 'Issuer details not found.', templateNameAlreadyExist: 'Template name already exists for this issuer.', - deleteTemplate: 'Error while deleting template.' + deleteTemplate: 'Error while deleting template.', + failedDeleteTemplate: '[createTemplate] compensating delete FAILED for templateId=' } }, oidcIssuerSession: { From 953ef8d404d07ac3fb56b336de635e0fcd0e89b1 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Tue, 28 Oct 2025 15:36:24 +0530 Subject: [PATCH 17/43] fix: added validityInfo for credential validity check Signed-off-by: Rinkal Bhojani --- .../dtos/issuer-sessions.dto.ts | 69 +- .../oid4vc-issuer-sessions.interfaces.ts | 14 +- .../interfaces/oid4vc-template.interfaces.ts | 2 +- .../helpers/credential-sessions.builder.ts | 750 ++++++++++-------- .../src/oid4vc-issuance.service.ts | 13 +- libs/common/src/date-only.ts | 38 + libs/common/src/response-messages/index.ts | 3 +- 7 files changed, 529 insertions(+), 360 deletions(-) create mode 100644 libs/common/src/date-only.ts 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 8d9630964..2325de600 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 @@ -21,6 +21,7 @@ import { } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; +import { dateToSeconds } from '@credebl/common/date-only'; /* ========= disclosureFrame custom validator ========= */ function isDisclosureFrameValue(v: unknown): boolean { @@ -117,6 +118,24 @@ function ExactlyOneOf(keys: string[], options?: ValidationOptions) { return Validate(ExactlyOneOfConstraint, keys, options); } +export class ValidityInfo { + @ApiProperty({ + example: '2025-04-23T14:34:09.188Z', + required: true + }) + @IsString() + @IsNotEmpty() + validFrom: Date; + + @ApiProperty({ + example: '2026-05-03T14:34:09.188Z', + required: true + }) + @IsString() + @IsNotEmpty() + validUntil: Date; +} + /* ========= Request DTOs ========= */ export class CredentialRequestDto { @ApiProperty({ @@ -137,13 +156,20 @@ export class CredentialRequestDto { payload!: Record; @ApiPropertyOptional({ - description: 'Selective disclosure: claim -> boolean (or nested map)', - example: { name: true, DOB: true, additionalProp3: false }, + example: { validFrom: '2025-04-23T14:34:09.188Z', validUntil: '2026-05-03T14:34:09.188Z' }, required: false }) @IsOptional() - @IsDisclosureFrame() - disclosureFrame?: Record>; + validityInfo?: ValidityInfo; + + // @ApiPropertyOptional({ + // description: 'Selective disclosure: claim -> boolean (or nested map)', + // example: { name: true, DOB: true, additionalProp3: false }, + // required: false + // }) + // @IsOptional() + // @IsDisclosureFrame() + // disclosureFrame?: Record>; } export class CreateOidcCredentialOfferDto { @@ -257,20 +283,33 @@ export class CredentialDto { @ApiProperty({ description: 'Credential payload (namespace data, validity info, etc.)', - example: { - namespaces: { - 'org.iso.23220.photoID.1': { - birth_date: '1970-02-14', - family_name: 'Müller-Lüdenscheid', - given_name: 'Ford Praxibetel', - document_number: 'LA001801M' + example: [ + { + namespaces: { + 'org.iso.23220.photoID.1': { + birth_date: '1970-02-14', + family_name: 'Müller-Lüdenscheid', + given_name: 'Ford Praxibetel', + document_number: 'LA001801M' + } + }, + validityInfo: { + validFrom: '2025-04-23T14:34:09.188Z', + validUntil: '2026-05-03T14:34:09.188Z' } }, - validityInfo: { - validFrom: '2025-04-23T14:34:09.188Z', - validUntil: '2026-05-03T14:34:09.188Z' + { + full_name: 'Garry', + address: { + street_address: 'M.G. Road', + locality: 'Pune', + country: 'India' + }, + iat: 1698151532, + nbf: dateToSeconds(new Date()), + exp: dateToSeconds(new Date(Date.now() + 5 * 365 * 24 * 60 * 60 * 1000)) } - } + ] }) @ValidateNested() payload: object; diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts index 69e8f97d1..afe54b472 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts @@ -30,19 +30,19 @@ export enum AuthenticationType { export type DisclosureFrame = Record>; export interface CredentialPayload { - validityInfo: { - validFrom: Date; - validUntil: Date; - }; [key: string]: unknown; // extensible for mDoc or other formats } export interface CredentialRequest { - credentialSupportedId?: string; + // credentialSupportedId?: string; templateId: string; - format: CredentialFormat; // "vc+sd-jwt" | "mso_mdoc" + // format: CredentialFormat; // "vc+sd-jwt" | "mso_mdoc" payload: CredentialPayload; // user-supplied payload (without vct) - disclosureFrame?: DisclosureFrame; // only relevant for vc+sd-jwt + // disclosureFrame?: DisclosureFrame; // only relevant for vc+sd-jwt + validityInfo?: { + validFrom: Date; + validUntil: Date; + }; } export interface CreateOidcCredentialOffer { diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts index 0e5c32b0b..1d1ee9510 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -43,7 +43,7 @@ export interface ClaimDisplay { } export interface Claim { - path: string[]; + path?: string[]; display?: ClaimDisplay[]; mandatory?: boolean; } diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 2abc31e68..52cf788c6 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ -import { Prisma, credential_templates } from '@prisma/client'; +import { credential_templates } from '@prisma/client'; import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; import { CredentialFormat } from '@credebl/enum/enum'; import { @@ -9,19 +9,22 @@ import { SdJwtTemplate } from 'apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces'; import { UnprocessableEntityException } from '@nestjs/common'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; +import { dateToSeconds } from '@credebl/common/date-only'; /* ============================================================================ Domain Types ============================================================================ */ -type ValueType = 'string' | 'date' | 'number' | 'boolean' | 'integer' | string; +// type ValueType = 'string' | 'date' | 'number' | 'boolean' | 'integer' | string; -interface TemplateAttribute { - display?: { name: string; locale: string }[]; - mandatory?: boolean; - value_type?: ValueType; -} -type TemplateAttributes = Record; +// interface TemplateAttribute { +// display?: { name: string; locale: string }[]; +// mandatory?: boolean; +// value_type?: ValueType; +// } +// type TemplateAttributes = Record; export enum SignerMethodOption { DID = 'did', @@ -30,10 +33,16 @@ export enum SignerMethodOption { export type DisclosureFrame = Record>; +export interface validityInfo { + validFrom: Date; + validUntil: Date; +} + export interface CredentialRequestDtoLike { templateId: string; payload: Record; - disclosureFrame?: DisclosureFrame; + validityInfo?: validityInfo; + // disclosureFrame?: DisclosureFrame; } export interface CreateOidcCredentialOfferDtoLike { @@ -107,10 +116,10 @@ export const DEFAULT_TXCODE = { Small Utilities ============================================================================ */ -const isNil = (value: unknown): value is null | undefined => null == value; -const isEmptyString = (value: unknown): boolean => 'string' === typeof value && '' === value.trim(); -const isPlainRecord = (value: unknown): value is Record => - Boolean(value) && 'object' === typeof value && !Array.isArray(value); +// const isNil = (value: unknown): value is null | undefined => null == value; +// const isEmptyString = (value: unknown): boolean => 'string' === typeof value && '' === value.trim(); +// const isPlainRecord = (value: unknown): value is Record => +// Boolean(value) && 'object' === typeof value && !Array.isArray(value); /** Map DB format string -> API enum */ function mapDbFormatToApiFormat(dbFormat: string): CredentialFormat { @@ -141,78 +150,78 @@ function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' { * - map: Record * - array: Array<{ path: string[], mandatory?: boolean, value_type?: string, display?: ... }> */ -function normalizeTemplateAttributes(rawAttributes: Prisma.JsonValue): TemplateAttributes { - // if already a plain record keyed by claim name, cast and return - if (isPlainRecord(rawAttributes) && !Array.isArray(rawAttributes)) { - // We still guard that values look like TemplateAttribute, but be permissive. - return rawAttributes as TemplateAttributes; - } - - // If attributes are an array (draft-15 style), convert to map - if (Array.isArray(rawAttributes)) { - const attributesArray = rawAttributes as unknown as any[]; - const normalizedMap: TemplateAttributes = {}; - for (const attributeEntry of attributesArray) { - if (!isPlainRecord(attributeEntry)) { - continue; // skip invalid entries - } - - // draft-15: path is array like ["org.iso.23220.photoID.1","given_name"] or ["name"] - const pathValue = attributeEntry.path; - if (!Array.isArray(pathValue) || 0 === pathValue.length) { - continue; - } - - // prefer last path element as local claim name (keeps namespace support) - const claimName = String(pathValue[pathValue.length - 1]); - - normalizedMap[claimName] = { - mandatory: Boolean(attributeEntry.mandatory), - value_type: attributeEntry.value_type ? String(attributeEntry.value_type) : undefined, - display: Array.isArray(attributeEntry.display) - ? attributeEntry.display.map((d: any) => ({ name: d.name, locale: d.locale })) - : undefined - }; - } - return normalizedMap; - } - - // if it's a JSON string, try parse - if ('string' === typeof rawAttributes) { - try { - const parsed = JSON.parse(rawAttributes); - return normalizeTemplateAttributes(parsed as Prisma.JsonValue); - } catch { - throw new Error('Invalid template.attributes JSON string'); - } - } - - throw new Error('Unrecognized template.attributes shape'); -} +// function normalizeTemplateAttributes(rawAttributes: Prisma.JsonValue): TemplateAttributes { +// // if already a plain record keyed by claim name, cast and return +// if (isPlainRecord(rawAttributes) && !Array.isArray(rawAttributes)) { +// // We still guard that values look like TemplateAttribute, but be permissive. +// return rawAttributes as TemplateAttributes; +// } + +// // If attributes are an array (draft-15 style), convert to map +// if (Array.isArray(rawAttributes)) { +// const attributesArray = rawAttributes as unknown as any[]; +// const normalizedMap: TemplateAttributes = {}; +// for (const attributeEntry of attributesArray) { +// if (!isPlainRecord(attributeEntry)) { +// continue; // skip invalid entries +// } + +// // draft-15: path is array like ["org.iso.23220.photoID.1","given_name"] or ["name"] +// const pathValue = attributeEntry.path; +// if (!Array.isArray(pathValue) || 0 === pathValue.length) { +// continue; +// } + +// // prefer last path element as local claim name (keeps namespace support) +// const claimName = String(pathValue[pathValue.length - 1]); + +// normalizedMap[claimName] = { +// mandatory: Boolean(attributeEntry.mandatory), +// value_type: attributeEntry.value_type ? String(attributeEntry.value_type) : undefined, +// display: Array.isArray(attributeEntry.display) +// ? attributeEntry.display.map((d: any) => ({ name: d.name, locale: d.locale })) +// : undefined +// }; +// } +// return normalizedMap; +// } + +// // if it's a JSON string, try parse +// if ('string' === typeof rawAttributes) { +// try { +// const parsed = JSON.parse(rawAttributes); +// return normalizeTemplateAttributes(parsed as Prisma.JsonValue); +// } catch { +// throw new Error('Invalid template.attributes JSON string'); +// } +// } + +// throw new Error('Unrecognized template.attributes shape'); +// } /* ============================================================================ Validation: Mandatory claims ============================================================================ */ -function assertMandatoryClaims( - payload: Record, - attributes: TemplateAttributes, - context: { templateId: string } -): void { - const missingClaims: string[] = []; - for (const [claimName, attributeDefinition] of Object.entries(attributes)) { - if (!attributeDefinition?.mandatory) { - continue; - } - const claimValue = payload[claimName]; - if (isNil(claimValue) || isEmptyString(claimValue)) { - missingClaims.push(claimName); - } - } - if (missingClaims.length) { - throw new Error(`Missing mandatory claims for template "${context.templateId}": ${missingClaims.join(', ')}`); - } -} +// function assertMandatoryClaims( +// payload: Record, +// attributes: TemplateAttributes, +// context: { templateId: string } +// ): void { +// const missingClaims: string[] = []; +// for (const [claimName, attributeDefinition] of Object.entries(attributes)) { +// if (!attributeDefinition?.mandatory) { +// continue; +// } +// const claimValue = payload[claimName]; +// if (isNil(claimValue) || isEmptyString(claimValue)) { +// missingClaims.push(claimName); +// } +// } +// if (missingClaims.length) { +// throw new Error(`Missing mandatory claims for template "${context.templateId}": ${missingClaims.join(', ')}`); +// } +// } /* ============================================================================ Per-format credential builders (separated for readability) @@ -221,120 +230,120 @@ function assertMandatoryClaims( ============================================================================ */ /** Build an SD-JWT credential object */ -function buildSdJwtCredential( - credentialRequest: CredentialRequestDtoLike, - templateRecord: credential_templates, - signerOptions?: SignerOption[] -): BuiltCredential { - // For SD-JWT format we expect payload to be a flat map of claims (no namespaces) - const payloadCopy = { ...(credentialRequest.payload as Record) }; - // Validate mandatory claims using normalized attributes from templateRecord - const normalizedAttributes = normalizeTemplateAttributes(templateRecord.attributes); - assertMandatoryClaims(payloadCopy, normalizedAttributes, { templateId: credentialRequest.templateId }); - - // strip vct if present per requirement - delete payloadCopy.vct; - - const apiFormat = mapDbFormatToApiFormat(templateRecord.format); - const idSuffix = formatSuffix(apiFormat); - const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; - - return { - credentialSupportedId, - signerOptions: signerOptions ? signerOptions[0] : undefined, - format: apiFormat, - payload: payloadCopy, - ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) - }; -} - -/** Build an MSO mdoc credential object - * - For mdocs we expect the payload to include a `namespaces` map (draft-15 style) - */ -function buildMdocCredential( - credentialRequest: CredentialRequestDtoLike, - templateRecord: credential_templates, - signerOptions?: SignerOption[] -): BuiltCredential { - const incomingPayload = { ...(credentialRequest.payload as Record) }; - - // Normalize attributes and ensure we know the expected claim names - const normalizedAttributes = normalizeTemplateAttributes(templateRecord.attributes); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const templateDoctype: string | undefined = (templateRecord as any).doctype ?? undefined; - const defaultNamespace = templateDoctype ?? templateRecord.name; - - // If caller provided already-namespaced payload, keep it; otherwise build a namespaces map - const workingPayload = { ...incomingPayload }; - if (!workingPayload.namespaces) { - const namespacesMap: Record> = {}; - // collect claims that match attribute names into the chosen namespace - for (const claimName of Object.keys(normalizedAttributes)) { - if (Object.prototype.hasOwnProperty.call(incomingPayload, claimName)) { - namespacesMap[defaultNamespace] = namespacesMap[defaultNamespace] ?? {}; - namespacesMap[defaultNamespace][claimName] = (incomingPayload as any)[claimName]; - // remove original flattened claim to avoid duplication - delete (workingPayload as any)[claimName]; - } - } - if (0 < Object.keys(namespacesMap).length) { - (workingPayload as any).namespaces = namespacesMap; - } - } else { - // ensure namespaces is a plain object - if (!isPlainRecord((workingPayload as any).namespaces)) { - throw new Error(`Invalid mdoc payload: 'namespaces' must be an object`); - } - } - - // Validate mandatory claims exist somewhere inside namespaces - const missingMandatoryClaims: string[] = []; - for (const [claimName, attributeDef] of Object.entries(normalizedAttributes)) { - if (!attributeDef?.mandatory) { - continue; - } - - let found = false; - const namespacesObj = (workingPayload as any).namespaces as Record; - if (namespacesObj && isPlainRecord(namespacesObj)) { - for (const nsKey of Object.keys(namespacesObj)) { - const nsContent = namespacesObj[nsKey]; - if (nsContent && Object.prototype.hasOwnProperty.call(nsContent, claimName)) { - const value = nsContent[claimName]; - if (!isNil(value) && !('string' === typeof value && '' === value.trim())) { - found = true; - break; - } - } - } - } - if (!found) { - missingMandatoryClaims.push(claimName); - } - } - if (missingMandatoryClaims.length) { - throw new Error( - `Missing mandatory namespaced claims for template "${credentialRequest.templateId}": ${missingMandatoryClaims.join( - ', ' - )}` - ); - } - - // strip vct if present - delete (workingPayload as Record).vct; - - const apiFormat = mapDbFormatToApiFormat(templateRecord.format); - const idSuffix = formatSuffix(apiFormat); - const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; - - return { - credentialSupportedId, - signerOptions: signerOptions ? signerOptions[0] : undefined, - format: apiFormat, - payload: workingPayload, - ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) - }; -} +// function buildSdJwtCredential( +// credentialRequest: CredentialRequestDtoLike, +// templateRecord: credential_templates, +// signerOptions?: SignerOption[] +// ): BuiltCredential { +// // For SD-JWT format we expect payload to be a flat map of claims (no namespaces) +// const payloadCopy = { ...(credentialRequest.payload as Record) }; +// // Validate mandatory claims using normalized attributes from templateRecord +// const normalizedAttributes = normalizeTemplateAttributes(templateRecord.attributes); +// assertMandatoryClaims(payloadCopy, normalizedAttributes, { templateId: credentialRequest.templateId }); + +// // strip vct if present per requirement +// delete payloadCopy.vct; + +// const apiFormat = mapDbFormatToApiFormat(templateRecord.format); +// const idSuffix = formatSuffix(apiFormat); +// const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + +// return { +// credentialSupportedId, +// signerOptions: signerOptions ? signerOptions[0] : undefined, +// format: apiFormat, +// payload: payloadCopy, +// ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) +// }; +// } + +// /** Build an MSO mdoc credential object +// * - For mdocs we expect the payload to include a `namespaces` map (draft-15 style) +// */ +// function buildMdocCredential( +// credentialRequest: CredentialRequestDtoLike, +// templateRecord: credential_templates, +// signerOptions?: SignerOption[] +// ): BuiltCredential { +// const incomingPayload = { ...(credentialRequest.payload as Record) }; + +// // Normalize attributes and ensure we know the expected claim names +// const normalizedAttributes = normalizeTemplateAttributes(templateRecord.attributes); +// // eslint-disable-next-line @typescript-eslint/no-explicit-any +// const templateDoctype: string | undefined = (templateRecord as any).doctype ?? undefined; +// const defaultNamespace = templateDoctype ?? templateRecord.name; + +// // If caller provided already-namespaced payload, keep it; otherwise build a namespaces map +// const workingPayload = { ...incomingPayload }; +// if (!workingPayload.namespaces) { +// const namespacesMap: Record> = {}; +// // collect claims that match attribute names into the chosen namespace +// for (const claimName of Object.keys(normalizedAttributes)) { +// if (Object.prototype.hasOwnProperty.call(incomingPayload, claimName)) { +// namespacesMap[defaultNamespace] = namespacesMap[defaultNamespace] ?? {}; +// namespacesMap[defaultNamespace][claimName] = (incomingPayload as any)[claimName]; +// // remove original flattened claim to avoid duplication +// delete (workingPayload as any)[claimName]; +// } +// } +// if (0 < Object.keys(namespacesMap).length) { +// (workingPayload as any).namespaces = namespacesMap; +// } +// } else { +// // ensure namespaces is a plain object +// if (!isPlainRecord((workingPayload as any).namespaces)) { +// throw new Error(`Invalid mdoc payload: 'namespaces' must be an object`); +// } +// } + +// // Validate mandatory claims exist somewhere inside namespaces +// const missingMandatoryClaims: string[] = []; +// for (const [claimName, attributeDef] of Object.entries(normalizedAttributes)) { +// if (!attributeDef?.mandatory) { +// continue; +// } + +// let found = false; +// const namespacesObj = (workingPayload as any).namespaces as Record; +// if (namespacesObj && isPlainRecord(namespacesObj)) { +// for (const nsKey of Object.keys(namespacesObj)) { +// const nsContent = namespacesObj[nsKey]; +// if (nsContent && Object.prototype.hasOwnProperty.call(nsContent, claimName)) { +// const value = nsContent[claimName]; +// if (!isNil(value) && !('string' === typeof value && '' === value.trim())) { +// found = true; +// break; +// } +// } +// } +// } +// if (!found) { +// missingMandatoryClaims.push(claimName); +// } +// } +// if (missingMandatoryClaims.length) { +// throw new Error( +// `Missing mandatory namespaced claims for template "${credentialRequest.templateId}": ${missingMandatoryClaims.join( +// ', ' +// )}` +// ); +// } + +// // strip vct if present +// delete (workingPayload as Record).vct; + +// const apiFormat = mapDbFormatToApiFormat(templateRecord.format); +// const idSuffix = formatSuffix(apiFormat); +// const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + +// return { +// credentialSupportedId, +// signerOptions: signerOptions ? signerOptions[0] : undefined, +// format: apiFormat, +// payload: workingPayload, +// ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) +// }; +// } /* ============================================================================ Main Builder: buildCredentialOfferPayload @@ -342,137 +351,137 @@ function buildMdocCredential( - Accepts `authorizationServerUrl` parameter; txCode is a constant above ============================================================================ */ -export function buildCredentialOfferPayload( - dto: CreateOidcCredentialOfferDtoLike, - templates: credential_templates[], - issuerDetails?: { - publicId: string; - authorizationServerUrl?: string; - }, - signerOptions?: SignerOption[] -): CredentialOfferPayload { - // Index templates by id - const templatesById = new Map(templates.map((template) => [template.id, template])); - - // Validate template ids - const missingTemplateIds = dto.credentials.map((c) => c.templateId).filter((id) => !templatesById.has(id)); - if (missingTemplateIds.length) { - throw new Error(`Unknown template ids: ${missingTemplateIds.join(', ')}`); - } - - // Build each credential using the template's format - const builtCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { - const templateRecord = templatesById.get(credentialRequest.templateId)!; - // we normalize attributes to support both draft-13 (map) and draft-15 (array) shapes - normalizeTemplateAttributes(templateRecord.attributes); - - const templateFormat = (templateRecord as any).format ?? 'vc+sd-jwt'; - const apiFormat = mapDbFormatToApiFormat(templateFormat); - - if (apiFormat === CredentialFormat.SdJwtVc) { - return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions); - } - if (apiFormat === CredentialFormat.Mdoc) { - return buildMdocCredential(credentialRequest, templateRecord, signerOptions); - } - throw new Error(`Unsupported template format for ${templateFormat}`); - }); - - // Base envelope: allow explicit publicIssuerId from DTO or fallback to issuerDetails.publicId - const publicIssuerIdFromDto = dto.publicIssuerId; - const publicIssuerIdFromIssuerDetails = issuerDetails?.publicId; - const finalPublicIssuerId = publicIssuerIdFromDto ?? publicIssuerIdFromIssuerDetails; - - const baseEnvelope: BuiltCredentialOfferBase = { - credentials: builtCredentials, - ...(finalPublicIssuerId ? { publicIssuerId: finalPublicIssuerId } : {}) - }; - - // Determine which authorization flow to return: - // Priority: - // 1) If issuerDetails.authorizationServerUrl is provided, return preAuthorizedCodeFlowConfig using DEFAULT_TXCODE - // 2) Else fall back to flows present in DTO (still enforce XOR) - const overrideAuthorizationServerUrl = issuerDetails?.authorizationServerUrl; - if (overrideAuthorizationServerUrl) { - if ('string' !== typeof overrideAuthorizationServerUrl || '' === overrideAuthorizationServerUrl.trim()) { - throw new Error('issuerDetails.authorizationServerUrl must be a non-empty string when provided'); - } - return { - ...baseEnvelope, - preAuthorizedCodeFlowConfig: { - txCode: DEFAULT_TXCODE, - authorizationServerUrl: overrideAuthorizationServerUrl - } - }; - } - - // No override provided — use what DTO carries (must be XOR) - const hasPreAuthFromDto = Boolean(dto.preAuthorizedCodeFlowConfig); - const hasAuthCodeFromDto = Boolean(dto.authorizationCodeFlowConfig); - if (hasPreAuthFromDto === hasAuthCodeFromDto) { - throw new Error('Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'); - } - if (hasPreAuthFromDto) { - return { - ...baseEnvelope, - preAuthorizedCodeFlowConfig: dto.preAuthorizedCodeFlowConfig! - }; - } - return { - ...baseEnvelope, - authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! - }; -} +// export function buildCredentialOfferPayload( +// dto: CreateOidcCredentialOfferDtoLike, +// templates: credential_templates[], +// issuerDetails?: { +// publicId: string; +// authorizationServerUrl?: string; +// }, +// signerOptions?: SignerOption[] +// ): CredentialOfferPayload { +// // Index templates by id +// const templatesById = new Map(templates.map((template) => [template.id, template])); + +// // Validate template ids +// const missingTemplateIds = dto.credentials.map((c) => c.templateId).filter((id) => !templatesById.has(id)); +// if (missingTemplateIds.length) { +// throw new Error(`Unknown template ids: ${missingTemplateIds.join(', ')}`); +// } + +// // Build each credential using the template's format +// const builtCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { +// const templateRecord = templatesById.get(credentialRequest.templateId)!; +// // we normalize attributes to support both draft-13 (map) and draft-15 (array) shapes +// normalizeTemplateAttributes(templateRecord.attributes); + +// const templateFormat = (templateRecord as any).format ?? 'vc+sd-jwt'; +// const apiFormat = mapDbFormatToApiFormat(templateFormat); + +// if (apiFormat === CredentialFormat.SdJwtVc) { +// return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions); +// } +// if (apiFormat === CredentialFormat.Mdoc) { +// return buildMdocCredential(credentialRequest, templateRecord, signerOptions); +// } +// throw new Error(`Unsupported template format for ${templateFormat}`); +// }); + +// // Base envelope: allow explicit publicIssuerId from DTO or fallback to issuerDetails.publicId +// const publicIssuerIdFromDto = dto.publicIssuerId; +// const publicIssuerIdFromIssuerDetails = issuerDetails?.publicId; +// const finalPublicIssuerId = publicIssuerIdFromDto ?? publicIssuerIdFromIssuerDetails; + +// const baseEnvelope: BuiltCredentialOfferBase = { +// credentials: builtCredentials, +// ...(finalPublicIssuerId ? { publicIssuerId: finalPublicIssuerId } : {}) +// }; + +// // Determine which authorization flow to return: +// // Priority: +// // 1) If issuerDetails.authorizationServerUrl is provided, return preAuthorizedCodeFlowConfig using DEFAULT_TXCODE +// // 2) Else fall back to flows present in DTO (still enforce XOR) +// const overrideAuthorizationServerUrl = issuerDetails?.authorizationServerUrl; +// if (overrideAuthorizationServerUrl) { +// if ('string' !== typeof overrideAuthorizationServerUrl || '' === overrideAuthorizationServerUrl.trim()) { +// throw new Error('issuerDetails.authorizationServerUrl must be a non-empty string when provided'); +// } +// return { +// ...baseEnvelope, +// preAuthorizedCodeFlowConfig: { +// txCode: DEFAULT_TXCODE, +// authorizationServerUrl: overrideAuthorizationServerUrl +// } +// }; +// } + +// // No override provided — use what DTO carries (must be XOR) +// const hasPreAuthFromDto = Boolean(dto.preAuthorizedCodeFlowConfig); +// const hasAuthCodeFromDto = Boolean(dto.authorizationCodeFlowConfig); +// if (hasPreAuthFromDto === hasAuthCodeFromDto) { +// throw new Error('Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'); +// } +// if (hasPreAuthFromDto) { +// return { +// ...baseEnvelope, +// preAuthorizedCodeFlowConfig: dto.preAuthorizedCodeFlowConfig! +// }; +// } +// return { +// ...baseEnvelope, +// authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! +// }; +// } /* ============================================================================ Update Credential Offer builder (keeps behavior, clearer names) ============================================================================ */ -export function buildUpdateCredentialOfferPayload( - dto: CreateOidcCredentialOfferDtoLike, - templates: credential_templates[] -): { credentials: BuiltCredential[] } { - const templatesById = new Map(templates.map((template) => [template.id, template])); - - const missingTemplateIds = dto.credentials.map((c) => c.templateId).filter((id) => !templatesById.has(id)); - if (missingTemplateIds.length) { - throw new Error(`Unknown template ids: ${missingTemplateIds.join(', ')}`); - } - - const normalizedCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { - const templateRecord = templatesById.get(credentialRequest.templateId)!; - - // Normalize attributes shape and ensure it's valid - const attributesMap = normalizeTemplateAttributes(templateRecord.attributes); - - // ensure payload keys match known attributes - const payloadKeys = Object.keys(credentialRequest.payload); - const invalidPayloadKeys = payloadKeys.filter((payloadKey) => !attributesMap[payloadKey]); - if (invalidPayloadKeys.length) { - throw new Error( - `Invalid attributes for template "${credentialRequest.templateId}": ${invalidPayloadKeys.join(', ')}` - ); - } - - // Validate mandatory claims - assertMandatoryClaims(credentialRequest.payload, attributesMap, { templateId: credentialRequest.templateId }); - - const selectedApiFormat = mapDbFormatToApiFormat(templateRecord.format); - const idSuffix = formatSuffix(selectedApiFormat); - const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; - - return { - credentialSupportedId, - format: selectedApiFormat, - payload: credentialRequest.payload, - ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) - }; - }); - - return { - credentials: normalizedCredentials - }; -} +// export function buildUpdateCredentialOfferPayload( +// dto: CreateOidcCredentialOfferDtoLike, +// templates: credential_templates[] +// ): { credentials: BuiltCredential[] } { +// const templatesById = new Map(templates.map((template) => [template.id, template])); + +// const missingTemplateIds = dto.credentials.map((c) => c.templateId).filter((id) => !templatesById.has(id)); +// if (missingTemplateIds.length) { +// throw new Error(`Unknown template ids: ${missingTemplateIds.join(', ')}`); +// } + +// const normalizedCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { +// const templateRecord = templatesById.get(credentialRequest.templateId)!; + +// // Normalize attributes shape and ensure it's valid +// const attributesMap = normalizeTemplateAttributes(templateRecord.attributes); + +// // ensure payload keys match known attributes +// const payloadKeys = Object.keys(credentialRequest.payload); +// const invalidPayloadKeys = payloadKeys.filter((payloadKey) => !attributesMap[payloadKey]); +// if (invalidPayloadKeys.length) { +// throw new Error( +// `Invalid attributes for template "${credentialRequest.templateId}": ${invalidPayloadKeys.join(', ')}` +// ); +// } + +// // Validate mandatory claims +// assertMandatoryClaims(credentialRequest.payload, attributesMap, { templateId: credentialRequest.templateId }); + +// const selectedApiFormat = mapDbFormatToApiFormat(templateRecord.format); +// const idSuffix = formatSuffix(selectedApiFormat); +// const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; + +// return { +// credentialSupportedId, +// format: selectedApiFormat, +// payload: credentialRequest.payload, +// ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) +// }; +// }); + +// return { +// credentials: normalizedCredentials +// }; +// } export function buildCredentialOfferUrl(baseUrl: string, getAllCredentialOffer: GetAllCredentialOffer): string { const criteriaParams: string[] = []; @@ -576,17 +585,76 @@ function buildDisclosureFrameFromTemplate(template: { attributes: CredentialAttr return disclosureFrame; } +function validateCredentialDatesInCertificateWindow(credentialValidityInfo: validityInfo, certificate) { + // Extract dates from credential + const credentialValidFrom = new Date(credentialValidityInfo.validFrom); + const credentialValidTo = new Date(credentialValidityInfo.validUntil); + + // Extract dates from certificate + const certNotBefore = new Date(certificate.validFrom); + const certNotAfter = new Date(certificate.expiry); + + // Validate that credential dates are within certificate validity period + const isCredentialStartValid = credentialValidFrom >= certNotBefore; + const isCredentialEndValid = credentialValidTo <= certNotAfter; + const isCredentialDurationValid = credentialValidFrom <= credentialValidTo; + + return { + isValid: isCredentialStartValid && isCredentialEndValid && isCredentialDurationValid, + details: { + credentialStartValid: isCredentialStartValid, + credentialEndValid: isCredentialEndValid, + credentialDurationValid: isCredentialDurationValid, + credentialValidFrom: credentialValidFrom.toISOString(), + credentialValidTo: credentialValidTo.toISOString(), + certificateNotBefore: certNotBefore.toISOString(), + certificateNotAfter: certNotAfter.toISOString() + } + }; +} + function buildSdJwtCredentialNew( credentialRequest: CredentialRequestDtoLike, templateRecord: any, - signerOptions?: SignerOption[] + signerOptions: SignerOption[], + activeCertificateDetails?: X509CertificateRecord[] ): BuiltCredential { // For SD-JWT format we expect payload to be a flat map of claims (no namespaces) - const payloadCopy = { ...(credentialRequest.payload as Record) }; + let payloadCopy = { ...(credentialRequest.payload as Record) }; // // strip vct if present per requirement // delete payloadCopy.vct; + if (signerOptions[0].method === SignerMethodOption.X5C && credentialRequest.validityInfo) { + const certificateDetail = activeCertificateDetails.find((x) => x.certificateBase64 === signerOptions[0].x5c[0]); + const validationResult = validateCredentialDatesInCertificateWindow( + credentialRequest.validityInfo, + certificateDetail + ); + if (!validationResult.isValid) { + throw new UnprocessableEntityException(`${JSON.stringify(validationResult.details)}`); + } + } + + 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)}`); + } + payloadCopy = { + ...payloadCopy, + nbf: dateToSeconds(credentialValidFrom), + exp: dateToSeconds(credentialValidTo) + }; + } + const sdJwtTemplate = templateRecord.attributes as SdJwtTemplate; payloadCopy.vct = sdJwtTemplate.vct; @@ -610,9 +678,32 @@ function buildSdJwtCredentialNew( function buildMdocCredentialNew( credentialRequest: CredentialRequestDtoLike, templateRecord: any, - signerOptions?: SignerOption[] + signerOptions: SignerOption[], + activeCertificateDetails: X509CertificateRecord[] ): BuiltCredential { - const incomingPayload = { ...(credentialRequest.payload as Record) }; + let incomingPayload = { ...(credentialRequest.payload as Record) }; + + if ( + !credentialRequest.validityInfo || + !credentialRequest.validityInfo.validFrom || + !credentialRequest.validityInfo.validUntil + ) { + throw new UnprocessableEntityException(`${ResponseMessages.oidcIssuerSession.error.missingValidityInfo}`); + } + + const certificateDetail = activeCertificateDetails.find((x) => x.certificateBase64 === signerOptions[0].x5c[0]); + const validationResult = validateCredentialDatesInCertificateWindow( + credentialRequest.validityInfo, + certificateDetail + ); + + if (!validationResult.isValid) { + throw new UnprocessableEntityException(`${JSON.stringify(validationResult.details)}`); + } + incomingPayload = { + ...incomingPayload, + validityInfo: credentialRequest.validityInfo + }; // // If caller provided already-namespaced payload, keep it; otherwise build a namespaces map // const workingPayload = { ...incomingPayload }; @@ -645,8 +736,7 @@ function buildMdocCredentialNew( credentialSupportedId, signerOptions: signerOptions ? signerOptions[0] : undefined, format: apiFormat, - payload: incomingPayload, - ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) + payload: incomingPayload }; } @@ -657,7 +747,8 @@ export function buildCredentialOfferPayloadNew( publicId: string; authorizationServerUrl?: string; }, - signerOptions?: SignerOption[] + signerOptions?: SignerOption[], + activeCertificateDetails?: X509CertificateRecord[] ): CredentialOfferPayload { // Index templates by id const templatesById = new Map(templates.map((template) => [template.id, template])); @@ -679,12 +770,11 @@ export function buildCredentialOfferPayloadNew( const templateFormat = (templateRecord as any).format ?? 'vc+sd-jwt'; const apiFormat = mapDbFormatToApiFormat(templateFormat); - if (apiFormat === CredentialFormat.SdJwtVc) { - return buildSdJwtCredentialNew(credentialRequest, templateRecord, signerOptions); + return buildSdJwtCredentialNew(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); } if (apiFormat === CredentialFormat.Mdoc) { - return buildMdocCredentialNew(credentialRequest, templateRecord, signerOptions); + return buildMdocCredentialNew(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); } throw new Error(`Unsupported template format for ${templateFormat}`); }); diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index d4f8f193e..d877de735 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -53,13 +53,14 @@ import { UpdateCredentialRequest } from '../interfaces/oid4vc-issuer-sessions.interfaces'; import { - buildCredentialOfferPayload, + // buildCredentialOfferPayload, buildCredentialOfferPayloadNew, buildCredentialOfferUrl, CredentialOfferPayload } from '../libs/helpers/credential-sessions.builder'; import { x5cKeyType } from '@credebl/enum/enum'; import { instanceToPlain, plainToInstance } from 'class-transformer'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; type CredentialDisplayItem = { logo?: { uri: string; alt_text?: string }; @@ -309,7 +310,6 @@ export class Oid4vcIssuanceService { issuerId, signerOption }; - console.log(`service - createTemplate: `, issuerId); // Persist in DB const createdTemplate = await this.oid4vcIssuanceRepository.createTemplate(issuerId, metadata); if (!createdTemplate) { @@ -539,6 +539,7 @@ export class Oid4vcIssuanceService { //TDOD: signerOption should be under credentials change this with x509 support const signerOptions = []; + const activeCertificateDetails: X509CertificateRecord[] = []; for (const template of getAllOfferTemplates) { if (template.signerOption === SignerOption.DID) { signerOptions.push({ @@ -560,6 +561,7 @@ export class Oid4vcIssuanceService { method: SignerMethodOption.X5C, x5c: [activeCertificate.certificateBase64] }); + activeCertificateDetails.push(activeCertificate); } if (template.signerOption == SignerOption.X509_ED25519) { @@ -575,6 +577,7 @@ export class Oid4vcIssuanceService { method: SignerMethodOption.X5C, x5c: [activeCertificate.certificateBase64] }); + activeCertificateDetails.push(activeCertificate); } } //TODO: Implement x509 support and discuss with team @@ -589,7 +592,8 @@ export class Oid4vcIssuanceService { publicId: publicIssuerId, authorizationServerUrl: `${authorizationServerUrl}/oid4vci/${publicIssuerId}` }, - signerOptions as any + signerOptions as any, + activeCertificateDetails ); console.log('This is the buildOidcCredentialOffer:', JSON.stringify(buildOidcCredentialOffer, null, 2)); @@ -763,11 +767,8 @@ export class Oid4vcIssuanceService { const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); const templates = await this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); - console.log(`---------------- emplates, configMetadata`, templates); const credentialConfigurationsSupported = buildCredentialConfigurationsSupportedNew(templates); - console.log(`-------------------credentialConfigurationsSupported`, credentialConfigurationsSupported); - return buildIssuerPayload({ credentialConfigurationsSupported }, issuerDetails); } catch (error) { this.logger.error(`[buildOidcIssuerPayload] - error: ${JSON.stringify(error)}`); diff --git a/libs/common/src/date-only.ts b/libs/common/src/date-only.ts new file mode 100644 index 000000000..969a4ce2f --- /dev/null +++ b/libs/common/src/date-only.ts @@ -0,0 +1,38 @@ +const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); +export class DateOnly { + private date: Date; + + public constructor(date?: string) { + this.date = date ? new Date(date) : new Date(); + } + + get [Symbol.toStringTag](): string { + return DateOnly.name; + } + + toString(): string { + return this.toISOString(); + } + + toJSON(): string { + return this.toISOString(); + } + + toISOString(): string { + return this.date.toISOString().split('T')[0]; + } + + [customInspectSymbol](): string { + return this.toISOString(); + } +} + +export const oneDayInMilliseconds = 24 * 60 * 60 * 1000; +export const tenDaysInMilliseconds = 10 * oneDayInMilliseconds; +export const oneYearInMilliseconds = 365 * oneDayInMilliseconds; +export const serverStartupTimeInMilliseconds = Date.now(); + +export function dateToSeconds(date: Date | DateOnly): number { + const realDate = date instanceof DateOnly ? new Date(date.toISOString()) : date; + return Math.floor(realDate.getTime() / 1000); +} diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 653e6eeca..e4a5647e9 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -553,7 +553,8 @@ export const ResponseMessages = { error: { errorCreateOffer: 'Error while creating OID4VC credential offer on agent.', errorUpdateOffer: 'Error while updating OID4VC credential offer on agent.', - deleteFailed: 'Failed to delete OID4VC credential offer.' + deleteFailed: 'Failed to delete OID4VC credential offer.', + missingValidityInfo: 'Validity Info(validFrom, validTo) is required for validity of credential' } }, x509: { From 25c6b13c4485def59350f47d7cbeac68b207055a Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Tue, 28 Oct 2025 19:09:52 +0530 Subject: [PATCH 18/43] feat: added docker file Signed-off-by: Tipu_Singh --- Dockerfiles/Dockerfile.x509 | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 Dockerfiles/Dockerfile.x509 diff --git a/Dockerfiles/Dockerfile.x509 b/Dockerfiles/Dockerfile.x509 new file mode 100644 index 000000000..6d15a054f --- /dev/null +++ b/Dockerfiles/Dockerfile.x509 @@ -0,0 +1,45 @@ +# Stage 1: Build the application +FROM node:18-alpine as build +# Install OpenSSL +RUN apk add --no-cache openssl +RUN npm install -g pnpm +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +COPY pnpm-workspace.yaml ./ +#COPY package-lock.json ./ + +ENV PUPPETEER_SKIP_DOWNLOAD=true + +# Install dependencies while ignoring scripts (including Puppeteer's installation) +RUN pnpm i --ignore-scripts + +# Copy the rest of the application code +COPY . . +# RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate +RUN cd libs/prisma-service && npx prisma generate + +# Build the x509 service +RUN npm run build x509 + + +# Stage 2: Create the final image +FROM node:18-alpine +# Install OpenSSL +RUN apk add --no-cache openssl +# RUN npm install -g pnpm +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/x509/ ./dist/apps/x509/ + +# Copy the libs folder from the build stage +COPY --from=build /app/libs/ ./libs/ +#COPY --from=build /app/package.json ./ +COPY --from=build /app/node_modules ./node_modules + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/x509/main.js"] \ No newline at end of file From ffaa6721a3a2b1d34ab18a4c18a1797822134156 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Wed, 29 Oct 2025 17:06:13 +0530 Subject: [PATCH 19/43] removed commented code and refactored Signed-off-by: Rinkal Bhojani --- .../dtos/oid4vc-issuer-template.dto.ts | 27 -- .../oid4vc-issuance/dtos/oid4vc-issuer.dto.ts | 96 ---- .../oid4vc-issuer-sessions.interfaces.ts | 6 +- .../interfaces/oid4vc-template.interfaces.ts | 13 +- .../credential-sessions-mdoc.builder.ts | 449 ------------------ .../helpers/credential-sessions.builder.ts | 379 +-------------- .../libs/helpers/issuer.metadata.ts | 126 +---- .../src/oid4vc-issuance.service.ts | 17 +- 8 files changed, 13 insertions(+), 1100 deletions(-) delete mode 100644 apps/oid4vc-issuance/libs/helpers/credential-sessions-mdoc.builder.ts 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 f3578d4f4..13a6a98e6 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 @@ -29,33 +29,6 @@ class CredentialAttributeDisplayDto { locale?: string; } -// export class CredentialAttributeDto { -// @ApiProperty({ required: false, description: 'Whether the attribute is mandatory' }) -// @IsOptional() -// @IsBoolean() -// mandatory?: boolean; - -// // TODO: Check how do we handle claims with only path rpoperty like email, etc. -// @ApiProperty({ description: 'Type of the attribute value (string, number, date, etc.)' }) -// @IsString() -// value_type: string; - -// @ApiProperty({ -// type: [String], -// description: -// 'Claims path pointer as per the draft 15 - https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID2.html#name-claims-path-pointer' -// }) -// @IsArray() -// @IsString({ each: true }) -// path: string[]; - -// @ApiProperty({ type: [CredentialAttributeDisplayDto], required: false, description: 'Localized display values' }) -// @IsOptional() -// @ValidateNested({ each: true }) -// @Type(() => CredentialAttributeDisplayDto) -// display?: CredentialAttributeDisplayDto[]; -// } - export enum AttributeType { STRING = 'string', NUMBER = 'number', diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts index cd99e117b..c0c340d38 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -43,102 +43,6 @@ export class IssuerDisplayDto { logo?: LogoDto; } -// TODO: Check where it is used, coz no reference found -// @ApiExtraModels(ClaimDto) -// export class CredentialConfigurationDto { -// @ApiProperty({ -// description: 'The format of the credential', -// example: 'jwt_vc_json' -// }) -// @IsString() -// @IsDefined({ message: 'format field is required' }) -// @IsNotEmpty({ message: 'format property is required' }) -// format: string; - -// @ApiProperty({ required: false }) -// @IsOptional() -// @IsString() -// vct?: string; - -// @ApiProperty({ required: false }) -// @IsOptional() -// @IsString() -// doctype?: string; - -// @ApiProperty() -// @IsString() -// scope: string; - -// @ApiProperty({ -// description: 'List of claims supported in this credential', -// type: [ClaimDto] -// }) -// @IsArray() -// @ValidateNested({ each: true }) -// @Type(() => ClaimDto) -// claims: ClaimDto[]; -// // @ApiProperty({ -// // description: 'Claims supported by this credential', -// // type: 'object', -// // additionalProperties: { $ref: getSchemaPath(ClaimDto) } -// // }) -// // @IsObject() -// // @ValidateNested({ each: true }) -// // @Transform(({ value }) => -// // Object.fromEntries(Object.entries(value || {}).map(([k, v]) => [k, plainToInstance(ClaimDto, v)])) -// // ) -// // claims: Record; - -// @ApiProperty({ type: [String] }) -// @IsArray() -// credential_signing_alg_values_supported: string[]; - -// @ApiProperty({ type: [String] }) -// @IsArray() -// cryptographic_binding_methods_supported: string[]; - -// @ApiProperty({ -// description: 'Localized display information for the credential', -// type: [DisplayDto] -// }) -// @IsArray() -// @ValidateNested({ each: true }) -// @Type(() => DisplayDto) -// display: DisplayDto[]; -// } - -// export class AuthorizationServerConfigDto { -// @ApiProperty({ -// description: 'Authorization server issuer URL', -// example: 'https://auth.credebl.com', -// }) -// @IsUrl() -// issuer: string - -// @ApiPropertyOptional({ -// description: 'Token endpoint of the authorization server', -// example: 'https://auth.credebl.com/oauth/token', -// }) -// @IsOptional() -// @IsUrl() -// token_endpoint: string - -// @ApiProperty({ -// description: 'Authorization endpoint of the server', -// example: 'https://auth.credebl.com/oauth/authorize', -// }) -// @IsUrl() -// authorization_endpoint: string - -// @ApiProperty({ -// description: 'Supported scopes', -// example: ['openid', 'profile', 'email'], -// }) -// @IsArray() -// @IsString({ each: true }) -// scopes_supported: string[] -// } - export class ClientAuthenticationDto { @ApiProperty({ description: 'OAuth2 client ID for the authorization server', diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts index afe54b472..9a266a8ee 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts @@ -34,11 +34,8 @@ export interface CredentialPayload { } export interface CredentialRequest { - // credentialSupportedId?: string; templateId: string; - // format: CredentialFormat; // "vc+sd-jwt" | "mso_mdoc" - payload: CredentialPayload; // user-supplied payload (without vct) - // disclosureFrame?: DisclosureFrame; // only relevant for vc+sd-jwt + payload: CredentialPayload; validityInfo?: { validFrom: Date; validUntil: Date; @@ -47,7 +44,6 @@ export interface CredentialRequest { export interface CreateOidcCredentialOffer { // e.g. "abc-gov" - // signerMethod: SignerMethodOption; // only option selector authenticationType: AuthenticationType; // only option selector credentials: CredentialRequest[]; // one or more credentials } diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts index 1d1ee9510..5f590389c 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -1,12 +1,5 @@ import { Prisma, SignerOption } from '@prisma/client'; import { CredentialFormat } from '@credebl/enum/enum'; - -// export interface CredentialAttribute { -// mandatory?: boolean; -// value_type: string; -// display?: Display[]; -// } - export interface SdJwtTemplate { vct: string; attributes: CredentialAttribute[]; @@ -23,15 +16,11 @@ export interface MdocTemplate { export interface CreateCredentialTemplate { name: string; description?: string; - signerOption?: SignerOption; //SignerOption; + signerOption?: SignerOption; format: CredentialFormat; canBeRevoked: boolean; - // attributes: Prisma.JsonValue; appearance?: Prisma.JsonValue; issuerId: string; - // vct?: string; - // doctype?: string; - template: SdJwtTemplate | MdocTemplate; } diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions-mdoc.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions-mdoc.builder.ts deleted file mode 100644 index 7d892a89f..000000000 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions-mdoc.builder.ts +++ /dev/null @@ -1,449 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ -import { Prisma, credential_templates } from '@prisma/client'; -import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; -import { CredentialFormat } from '@credebl/enum/enum'; -/* ============================================================================ - Domain Types -============================================================================ */ - -type ValueType = 'string' | 'date' | 'number' | 'boolean' | 'integer' | string; - -interface TemplateAttribute { - display: { name: string; locale: string }[]; - mandatory: boolean; - value_type: ValueType; -} -type TemplateAttributes = Record; - -export enum SignerMethodOption { - DID = 'did', - X5C = 'x5c' -} - -export type DisclosureFrame = Record>; - -export interface CredentialRequestDtoLike { - templateId: string; - payload: Record; - disclosureFrame?: DisclosureFrame; -} - -export interface CreateOidcCredentialOfferDtoLike { - credentials: CredentialRequestDtoLike[]; - - // Exactly one of the two must be provided (XOR) - preAuthorizedCodeFlowConfig?: { - txCode: { description?: string; length: number; input_mode: 'numeric' | 'text' | 'alphanumeric' }; - authorizationServerUrl: string; - }; - authorizationCodeFlowConfig?: { - authorizationServerUrl: string; - }; - - // NOTE: issuerId is intentionally NOT emitted in the final payload - publicIssuerId?: string; -} - -export interface ResolvedSignerOption { - method: 'did' | 'x5c'; - did?: string; - x5c?: string[]; -} - -/* ============================================================================ - Strong return types -============================================================================ */ - -export interface BuiltCredential { - /** e.g., "BirthCertificateCredential-sdjwt" or "DrivingLicenseCredential-mdoc" */ - credentialSupportedId: string; - signerOptions?: ResolvedSignerOption; - /** Derived from template.format ("vc+sd-jwt" | "mdoc") */ - format: CredentialFormat; - /** User-provided payload (validated, with vct removed) */ - payload: Record; - /** Optional disclosure frame (usually for SD-JWT) */ - disclosureFrame?: DisclosureFrame; -} - -export interface BuiltCredentialOfferBase { - /** Resolved signer option (DID or x5c) */ - signerOption?: ResolvedSignerOption; - /** Normalized credential entries */ - credentials: BuiltCredential[]; - /** Optional public issuer id to include */ - publicIssuerId?: string; -} - -/** Final payload = base + EXACTLY ONE of the two flows */ -export type CredentialOfferPayload = BuiltCredentialOfferBase & - ( - | { - preAuthorizedCodeFlowConfig: { - txCode: { description?: string; length: number; input_mode: 'numeric' | 'text' | 'alphanumeric' }; - authorizationServerUrl: string; - }; - authorizationCodeFlowConfig?: never; - } - | { - authorizationCodeFlowConfig: { - authorizationServerUrl: string; - }; - preAuthorizedCodeFlowConfig?: never; - } - ); - -/* ============================================================================ - Small Utilities -============================================================================ */ - -const isNil = (value: unknown): value is null | undefined => null == value; -const isEmptyString = (value: unknown): boolean => 'string' === typeof value && '' === value.trim(); -const isPlainRecord = (value: unknown): value is Record => - Boolean(value) && 'object' === typeof value && !Array.isArray(value); - -/** Map DB format string -> API enum */ -function mapDbFormatToApiFormat(dbFormat: string): CredentialFormat { - if ('sd-jwt' === dbFormat || 'vc+sd-jwt' === dbFormat || 'sdjwt' === dbFormat || 'sd+jwt-vc' === dbFormat) { - return CredentialFormat.SdJwtVc; - } - if ('mso_mdoc' === dbFormat) { - return CredentialFormat.Mdoc; - } - throw new Error(`Unsupported template format: ${dbFormat}`); -} - -/** Map API enum -> id suffix required for credentialSupportedId */ -function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' { - return apiFormat === CredentialFormat.SdJwtVc ? 'sdjwt' : 'mdoc'; -} - -/* ============================================================================ - Validation of Payload vs Template Attributes -============================================================================ */ - -/** Throw if any template-mandatory claim is missing/empty in payload. */ -function assertMandatoryClaims( - payload: Record, - attributes: TemplateAttributes, - context: { templateId: string } -): void { - const missingClaims: string[] = []; - for (const [claimName, attributeDefinition] of Object.entries(attributes)) { - if (!attributeDefinition?.mandatory) { - continue; - } - const claimValue = payload[claimName]; - if (isNil(claimValue) || isEmptyString(claimValue)) { - missingClaims.push(claimName); - } - } - if (missingClaims.length) { - throw new Error(`Missing mandatory claims for template "${context.templateId}": ${missingClaims.join(', ')}`); - } -} - -/* ============================================================================ - JsonValue → TemplateAttributes Narrowing (Type Guards) -============================================================================ */ - -function isDisplayArray(value: unknown): value is { name: string; locale: string }[] { - return ( - Array.isArray(value) && - value.every( - (entry) => - isPlainRecord(entry) && 'string' === typeof (entry as any).name && 'string' === typeof (entry as any).locale - ) - ); -} - -/* ============================================================================ - Improved ensureTemplateAttributes: runtime assert with helpful errors -============================================================================ */ - -const ALLOWED_VALUE_TYPES: ValueType[] = ['string', 'date', 'number', 'boolean', 'integer']; - -function ensureTemplateAttributes(jsonValue: Prisma.JsonValue): TemplateAttributes { - if (!isPlainRecord(jsonValue)) { - throw new Error( - `Invalid template.attributes: expected an object map but received ${ - null === jsonValue ? 'null' : typeof jsonValue - }.\n\nFix: provide an object whose keys are attribute names and whose values are attribute definitions, e.g.\n{\n "given_name": { "mandatory": true, "value_type": "string" }\n}` - ); - } - - const attributesMap = jsonValue as Record; - const attributeKeys = Object.keys(attributesMap); - if (0 === attributeKeys.length) { - throw new Error( - 'Invalid template.attributes: object is empty (no attributes defined).\n\nFix: add at least one attribute definition, for example:\n{\n "given_name": { "mandatory": true, "value_type": "string" }\n}' - ); - } - - const problems: string[] = []; - const suggestedFixes: string[] = []; - - for (const attributeKey of attributeKeys) { - const rawAttributeDef = attributesMap[attributeKey]; - - if (!isPlainRecord(rawAttributeDef)) { - problems.push( - `${attributeKey}: expected an object but got ${null === rawAttributeDef ? 'null' : typeof rawAttributeDef}` - ); - suggestedFixes.push( - `Replace attribute "${attributeKey}" value with an object, e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); - continue; - } - - // mandatory checks - if (!('mandatory' in rawAttributeDef)) { - problems.push(`${attributeKey}.mandatory: missing`); - suggestedFixes.push( - `Add mandatory boolean for "${attributeKey}":\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); - } else if ('boolean' !== typeof (rawAttributeDef as any).mandatory) { - problems.push(`${attributeKey}.mandatory: expected boolean but got ${typeof (rawAttributeDef as any).mandatory}`); - suggestedFixes.push( - `Set "mandatory" to a boolean for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); - } - - // value_type checks - if (!('value_type' in rawAttributeDef)) { - problems.push(`${attributeKey}.value_type: missing`); - suggestedFixes.push( - `Add value_type for "${attributeKey}", for example:\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); - } else if ('string' !== typeof (rawAttributeDef as any).value_type) { - problems.push( - `${attributeKey}.value_type: expected string but got ${typeof (rawAttributeDef as any).value_type}` - ); - suggestedFixes.push( - `Make sure "value_type" is a string for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); - } else { - const declaredType = (rawAttributeDef as any).value_type as string; - if (!ALLOWED_VALUE_TYPES.includes(declaredType as ValueType)) { - problems.push( - `${attributeKey}.value_type: unsupported value_type "${declaredType}". Allowed types: ${ALLOWED_VALUE_TYPES.join(', ')}` - ); - suggestedFixes.push( - `Use one of the allowed types for "${attributeKey}", e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string" }` - ); - } - } - - // display checks (optional) - if ('display' in rawAttributeDef && !isDisplayArray((rawAttributeDef as any).display)) { - problems.push(`${attributeKey}.display: expected array of { name: string, locale: string }`); - suggestedFixes.push( - `Fix "display" for "${attributeKey}" to be an array of objects with name/locale, e.g.\n"${attributeKey}": { "mandatory": true, "value_type": "string", "display": [{ "name": "Given Name", "locale": "en-US" }] }` - ); - } - } - - if (0 < problems.length) { - // Build a user-friendly message: problems + suggested fixes (unique) - const uniqueFixes = Array.from(new Set(suggestedFixes)).slice(0, 20); - const fixesText = uniqueFixes.length - ? `\n\nSuggested fixes (copy-paste examples):\n- ${uniqueFixes.join('\n- ')}` - : ''; - - // Include a small truncated sample of the attributes to help debugging - const samplePreview = JSON.stringify( - Object.fromEntries(attributeKeys.slice(0, 10).map((key) => [key, attributesMap[key]])), - (_, value) => { - if ('string' === typeof value && 200 < value.length) { - return `${value.slice(0, 200)}...`; - } - return value; - }, - 2 - ); - - throw new Error( - `Invalid template.attributes shape. Problems found:\n- ${problems.join( - '\n- ' - )}\n\nExample attributes (truncated):\n${samplePreview}${fixesText}` - ); - } - - // Safe to cast to TemplateAttributes - return attributesMap as TemplateAttributes; -} - -/* ============================================================================ - Builders -============================================================================ */ - -/** Build one credential block normalized to API format (using the template's format). */ -function buildOneCredential( - credentialRequest: CredentialRequestDtoLike, - templateRecord: credential_templates, - templateAttributes: TemplateAttributes, - signerOptions?: SignerOption[] -): BuiltCredential { - // 1) Validate payload against template attributes - assertMandatoryClaims(credentialRequest.payload, templateAttributes, { templateId: credentialRequest.templateId }); - - // 2) Decide API format from DB format - const selectedApiFormat = mapDbFormatToApiFormat(templateRecord.format); - - // 3) Build supportedId from template.name + suffix ("-sdjwt" | "-mdoc") - const idSuffix = formatSuffix(selectedApiFormat); - const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; - - // 4) Strip vct ALWAYS (per requirement) - const normalizedPayload = { ...(credentialRequest.payload as Record) }; - delete (normalizedPayload as Record).vct; - - return { - credentialSupportedId, // e.g., "BirthCertificateCredential-sdjwt" - signerOptions: signerOptions ? signerOptions[0] : undefined, - format: selectedApiFormat, // 'vc+sd-jwt' | 'mdoc' - payload: normalizedPayload, // without vct - ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) - }; -} - -/** - * Build the full OID4VC credential offer payload. - * - Verifies template IDs - * - Validates mandatory claims per template - * - Normalizes formats & IDs - * - Enforces XOR of flow configs - * - Removes issuerId from the final envelope - * - Removes vct from all payloads - * - Sets credentialSupportedId = "-sdjwt|mdoc" - */ -export function buildCredentialOfferPayload( - dto: CreateOidcCredentialOfferDtoLike, - templates: credential_templates[], - signerOptions?: SignerOption[] -): CredentialOfferPayload { - // Index templates - const templatesById = new Map(templates.map((template) => [template.id, template])); - - // Verify all requested templateIds exist - const unknownTemplateIds = dto.credentials - .map((c) => c.templateId) - .filter((requestedId) => !templatesById.has(requestedId)); - if (unknownTemplateIds.length) { - throw new Error(`Unknown template ids: ${unknownTemplateIds.join(', ')}`); - } - - // Build credentials - const builtCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { - const templateRecord = templatesById.get(credentialRequest.templateId)!; - const resolvedAttributes = ensureTemplateAttributes(templateRecord.attributes); // narrow JsonValue safely - return buildOneCredential(credentialRequest, templateRecord, resolvedAttributes, signerOptions); - }); - - // --- Base envelope (issuerId deliberately NOT included) --- - const baseEnvelope: BuiltCredentialOfferBase = { - credentials: builtCredentials, - ...(dto.publicIssuerId ? { publicIssuerId: dto.publicIssuerId } : {}) - }; - - // XOR flow selection (defensive) - const hasPreAuthFlow = Boolean(dto.preAuthorizedCodeFlowConfig); - const hasAuthCodeFlow = Boolean(dto.authorizationCodeFlowConfig); - if (hasPreAuthFlow === hasAuthCodeFlow) { - throw new Error('Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'); - } - - if (hasPreAuthFlow) { - return { - ...baseEnvelope, - preAuthorizedCodeFlowConfig: dto.preAuthorizedCodeFlowConfig! - }; - } - - return { - ...baseEnvelope, - authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! - }; -} - -// ----------------------------------------------------------------------------- -// Builder: Update Credential Offer -// ----------------------------------------------------------------------------- -export function buildUpdateCredentialOfferPayload( - dto: CreateOidcCredentialOfferDtoLike, - templates: credential_templates[] -): { credentials: BuiltCredential[] } { - // Index templates by id - const templatesById = new Map(templates.map((template) => [template.id, template])); - - // Validate all templateIds exist - const unknownTemplateIds = dto.credentials - .map((c) => c.templateId) - .filter((requestedId) => !templatesById.has(requestedId)); - if (unknownTemplateIds.length) { - throw new Error(`Unknown template ids: ${unknownTemplateIds.join(', ')}`); - } - - // Validate each credential against its template - const normalizedCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { - const templateRecord = templatesById.get(credentialRequest.templateId)!; - const resolvedAttributes = ensureTemplateAttributes(templateRecord.attributes); // safely narrow JsonValue - - // check that all payload keys exist in template attributes - const payloadKeys = Object.keys(credentialRequest.payload); - const invalidPayloadKeys = payloadKeys.filter((payloadKey) => !resolvedAttributes[payloadKey]); - if (invalidPayloadKeys.length) { - throw new Error( - `Invalid attributes for template "${credentialRequest.templateId}": ${invalidPayloadKeys.join(', ')}` - ); - } - - // also validate mandatory fields are present - assertMandatoryClaims(credentialRequest.payload, resolvedAttributes, { templateId: credentialRequest.templateId }); - - // build minimal normalized credential (no vct, issuerId, etc.) - const selectedApiFormat = mapDbFormatToApiFormat(templateRecord.format); - const idSuffix = formatSuffix(selectedApiFormat); - const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; - - return { - credentialSupportedId, - format: selectedApiFormat, - payload: credentialRequest.payload, - ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) - }; - }); - - // Only return credentials array here (update flow doesn't need preAuth/auth configs) - return { - credentials: normalizedCredentials - }; -} - -export function buildCredentialOfferUrl(baseUrl: string, getAllCredentialOffer: GetAllCredentialOffer): string { - const criteriaParams: string[] = []; - - if (getAllCredentialOffer.publicIssuerId) { - criteriaParams.push(`publicIssuerId=${encodeURIComponent(getAllCredentialOffer.publicIssuerId)}`); - } - - if (getAllCredentialOffer.preAuthorizedCode) { - criteriaParams.push(`preAuthorizedCode=${encodeURIComponent(getAllCredentialOffer.preAuthorizedCode)}`); - } - - if (getAllCredentialOffer.state) { - criteriaParams.push(`state=${encodeURIComponent(getAllCredentialOffer.state)}`); - } - - if (getAllCredentialOffer.credentialOfferUri) { - criteriaParams.push(`credentialOfferUri=${encodeURIComponent(getAllCredentialOffer.credentialOfferUri)}`); - } - - if (getAllCredentialOffer.authorizationCode) { - criteriaParams.push(`authorizationCode=${encodeURIComponent(getAllCredentialOffer.authorizationCode)}`); - } - - // Append query string if any params exist - return 0 < criteriaParams.length ? `${baseUrl}?${criteriaParams.join('&')}` : baseUrl; -} diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 52cf788c6..f473c827c 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -137,352 +137,6 @@ function formatSuffix(apiFormat: CredentialFormat): 'sdjwt' | 'mdoc' { return apiFormat === CredentialFormat.SdJwtVc ? 'sdjwt' : 'mdoc'; } -/* ============================================================================ - Template Attributes Normalization - - draft-13 used map: { given_name: { mandatory:true, value_type: "string" } } - - draft-15 returns attributes as array of attribute objects (with path) - This helper accepts both and normalizes to TemplateAttributes map. -============================================================================ */ - -/** - * Normalize attributes from DB/template into TemplateAttributes map. - * Accepts: - * - map: Record - * - array: Array<{ path: string[], mandatory?: boolean, value_type?: string, display?: ... }> - */ -// function normalizeTemplateAttributes(rawAttributes: Prisma.JsonValue): TemplateAttributes { -// // if already a plain record keyed by claim name, cast and return -// if (isPlainRecord(rawAttributes) && !Array.isArray(rawAttributes)) { -// // We still guard that values look like TemplateAttribute, but be permissive. -// return rawAttributes as TemplateAttributes; -// } - -// // If attributes are an array (draft-15 style), convert to map -// if (Array.isArray(rawAttributes)) { -// const attributesArray = rawAttributes as unknown as any[]; -// const normalizedMap: TemplateAttributes = {}; -// for (const attributeEntry of attributesArray) { -// if (!isPlainRecord(attributeEntry)) { -// continue; // skip invalid entries -// } - -// // draft-15: path is array like ["org.iso.23220.photoID.1","given_name"] or ["name"] -// const pathValue = attributeEntry.path; -// if (!Array.isArray(pathValue) || 0 === pathValue.length) { -// continue; -// } - -// // prefer last path element as local claim name (keeps namespace support) -// const claimName = String(pathValue[pathValue.length - 1]); - -// normalizedMap[claimName] = { -// mandatory: Boolean(attributeEntry.mandatory), -// value_type: attributeEntry.value_type ? String(attributeEntry.value_type) : undefined, -// display: Array.isArray(attributeEntry.display) -// ? attributeEntry.display.map((d: any) => ({ name: d.name, locale: d.locale })) -// : undefined -// }; -// } -// return normalizedMap; -// } - -// // if it's a JSON string, try parse -// if ('string' === typeof rawAttributes) { -// try { -// const parsed = JSON.parse(rawAttributes); -// return normalizeTemplateAttributes(parsed as Prisma.JsonValue); -// } catch { -// throw new Error('Invalid template.attributes JSON string'); -// } -// } - -// throw new Error('Unrecognized template.attributes shape'); -// } - -/* ============================================================================ - Validation: Mandatory claims -============================================================================ */ - -// function assertMandatoryClaims( -// payload: Record, -// attributes: TemplateAttributes, -// context: { templateId: string } -// ): void { -// const missingClaims: string[] = []; -// for (const [claimName, attributeDefinition] of Object.entries(attributes)) { -// if (!attributeDefinition?.mandatory) { -// continue; -// } -// const claimValue = payload[claimName]; -// if (isNil(claimValue) || isEmptyString(claimValue)) { -// missingClaims.push(claimName); -// } -// } -// if (missingClaims.length) { -// throw new Error(`Missing mandatory claims for template "${context.templateId}": ${missingClaims.join(', ')}`); -// } -// } - -/* ============================================================================ - Per-format credential builders (separated for readability) - - buildSdJwtCredential - - buildMdocCredential -============================================================================ */ - -/** Build an SD-JWT credential object */ -// function buildSdJwtCredential( -// credentialRequest: CredentialRequestDtoLike, -// templateRecord: credential_templates, -// signerOptions?: SignerOption[] -// ): BuiltCredential { -// // For SD-JWT format we expect payload to be a flat map of claims (no namespaces) -// const payloadCopy = { ...(credentialRequest.payload as Record) }; -// // Validate mandatory claims using normalized attributes from templateRecord -// const normalizedAttributes = normalizeTemplateAttributes(templateRecord.attributes); -// assertMandatoryClaims(payloadCopy, normalizedAttributes, { templateId: credentialRequest.templateId }); - -// // strip vct if present per requirement -// delete payloadCopy.vct; - -// const apiFormat = mapDbFormatToApiFormat(templateRecord.format); -// const idSuffix = formatSuffix(apiFormat); -// const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; - -// return { -// credentialSupportedId, -// signerOptions: signerOptions ? signerOptions[0] : undefined, -// format: apiFormat, -// payload: payloadCopy, -// ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) -// }; -// } - -// /** Build an MSO mdoc credential object -// * - For mdocs we expect the payload to include a `namespaces` map (draft-15 style) -// */ -// function buildMdocCredential( -// credentialRequest: CredentialRequestDtoLike, -// templateRecord: credential_templates, -// signerOptions?: SignerOption[] -// ): BuiltCredential { -// const incomingPayload = { ...(credentialRequest.payload as Record) }; - -// // Normalize attributes and ensure we know the expected claim names -// const normalizedAttributes = normalizeTemplateAttributes(templateRecord.attributes); -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// const templateDoctype: string | undefined = (templateRecord as any).doctype ?? undefined; -// const defaultNamespace = templateDoctype ?? templateRecord.name; - -// // If caller provided already-namespaced payload, keep it; otherwise build a namespaces map -// const workingPayload = { ...incomingPayload }; -// if (!workingPayload.namespaces) { -// const namespacesMap: Record> = {}; -// // collect claims that match attribute names into the chosen namespace -// for (const claimName of Object.keys(normalizedAttributes)) { -// if (Object.prototype.hasOwnProperty.call(incomingPayload, claimName)) { -// namespacesMap[defaultNamespace] = namespacesMap[defaultNamespace] ?? {}; -// namespacesMap[defaultNamespace][claimName] = (incomingPayload as any)[claimName]; -// // remove original flattened claim to avoid duplication -// delete (workingPayload as any)[claimName]; -// } -// } -// if (0 < Object.keys(namespacesMap).length) { -// (workingPayload as any).namespaces = namespacesMap; -// } -// } else { -// // ensure namespaces is a plain object -// if (!isPlainRecord((workingPayload as any).namespaces)) { -// throw new Error(`Invalid mdoc payload: 'namespaces' must be an object`); -// } -// } - -// // Validate mandatory claims exist somewhere inside namespaces -// const missingMandatoryClaims: string[] = []; -// for (const [claimName, attributeDef] of Object.entries(normalizedAttributes)) { -// if (!attributeDef?.mandatory) { -// continue; -// } - -// let found = false; -// const namespacesObj = (workingPayload as any).namespaces as Record; -// if (namespacesObj && isPlainRecord(namespacesObj)) { -// for (const nsKey of Object.keys(namespacesObj)) { -// const nsContent = namespacesObj[nsKey]; -// if (nsContent && Object.prototype.hasOwnProperty.call(nsContent, claimName)) { -// const value = nsContent[claimName]; -// if (!isNil(value) && !('string' === typeof value && '' === value.trim())) { -// found = true; -// break; -// } -// } -// } -// } -// if (!found) { -// missingMandatoryClaims.push(claimName); -// } -// } -// if (missingMandatoryClaims.length) { -// throw new Error( -// `Missing mandatory namespaced claims for template "${credentialRequest.templateId}": ${missingMandatoryClaims.join( -// ', ' -// )}` -// ); -// } - -// // strip vct if present -// delete (workingPayload as Record).vct; - -// const apiFormat = mapDbFormatToApiFormat(templateRecord.format); -// const idSuffix = formatSuffix(apiFormat); -// const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; - -// return { -// credentialSupportedId, -// signerOptions: signerOptions ? signerOptions[0] : undefined, -// format: apiFormat, -// payload: workingPayload, -// ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) -// }; -// } - -/* ============================================================================ - Main Builder: buildCredentialOfferPayload - - Now delegates per-format build to the two helpers above - - Accepts `authorizationServerUrl` parameter; txCode is a constant above -============================================================================ */ - -// export function buildCredentialOfferPayload( -// dto: CreateOidcCredentialOfferDtoLike, -// templates: credential_templates[], -// issuerDetails?: { -// publicId: string; -// authorizationServerUrl?: string; -// }, -// signerOptions?: SignerOption[] -// ): CredentialOfferPayload { -// // Index templates by id -// const templatesById = new Map(templates.map((template) => [template.id, template])); - -// // Validate template ids -// const missingTemplateIds = dto.credentials.map((c) => c.templateId).filter((id) => !templatesById.has(id)); -// if (missingTemplateIds.length) { -// throw new Error(`Unknown template ids: ${missingTemplateIds.join(', ')}`); -// } - -// // Build each credential using the template's format -// const builtCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { -// const templateRecord = templatesById.get(credentialRequest.templateId)!; -// // we normalize attributes to support both draft-13 (map) and draft-15 (array) shapes -// normalizeTemplateAttributes(templateRecord.attributes); - -// const templateFormat = (templateRecord as any).format ?? 'vc+sd-jwt'; -// const apiFormat = mapDbFormatToApiFormat(templateFormat); - -// if (apiFormat === CredentialFormat.SdJwtVc) { -// return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions); -// } -// if (apiFormat === CredentialFormat.Mdoc) { -// return buildMdocCredential(credentialRequest, templateRecord, signerOptions); -// } -// throw new Error(`Unsupported template format for ${templateFormat}`); -// }); - -// // Base envelope: allow explicit publicIssuerId from DTO or fallback to issuerDetails.publicId -// const publicIssuerIdFromDto = dto.publicIssuerId; -// const publicIssuerIdFromIssuerDetails = issuerDetails?.publicId; -// const finalPublicIssuerId = publicIssuerIdFromDto ?? publicIssuerIdFromIssuerDetails; - -// const baseEnvelope: BuiltCredentialOfferBase = { -// credentials: builtCredentials, -// ...(finalPublicIssuerId ? { publicIssuerId: finalPublicIssuerId } : {}) -// }; - -// // Determine which authorization flow to return: -// // Priority: -// // 1) If issuerDetails.authorizationServerUrl is provided, return preAuthorizedCodeFlowConfig using DEFAULT_TXCODE -// // 2) Else fall back to flows present in DTO (still enforce XOR) -// const overrideAuthorizationServerUrl = issuerDetails?.authorizationServerUrl; -// if (overrideAuthorizationServerUrl) { -// if ('string' !== typeof overrideAuthorizationServerUrl || '' === overrideAuthorizationServerUrl.trim()) { -// throw new Error('issuerDetails.authorizationServerUrl must be a non-empty string when provided'); -// } -// return { -// ...baseEnvelope, -// preAuthorizedCodeFlowConfig: { -// txCode: DEFAULT_TXCODE, -// authorizationServerUrl: overrideAuthorizationServerUrl -// } -// }; -// } - -// // No override provided — use what DTO carries (must be XOR) -// const hasPreAuthFromDto = Boolean(dto.preAuthorizedCodeFlowConfig); -// const hasAuthCodeFromDto = Boolean(dto.authorizationCodeFlowConfig); -// if (hasPreAuthFromDto === hasAuthCodeFromDto) { -// throw new Error('Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.'); -// } -// if (hasPreAuthFromDto) { -// return { -// ...baseEnvelope, -// preAuthorizedCodeFlowConfig: dto.preAuthorizedCodeFlowConfig! -// }; -// } -// return { -// ...baseEnvelope, -// authorizationCodeFlowConfig: dto.authorizationCodeFlowConfig! -// }; -// } - -/* ============================================================================ - Update Credential Offer builder (keeps behavior, clearer names) -============================================================================ */ - -// export function buildUpdateCredentialOfferPayload( -// dto: CreateOidcCredentialOfferDtoLike, -// templates: credential_templates[] -// ): { credentials: BuiltCredential[] } { -// const templatesById = new Map(templates.map((template) => [template.id, template])); - -// const missingTemplateIds = dto.credentials.map((c) => c.templateId).filter((id) => !templatesById.has(id)); -// if (missingTemplateIds.length) { -// throw new Error(`Unknown template ids: ${missingTemplateIds.join(', ')}`); -// } - -// const normalizedCredentials: BuiltCredential[] = dto.credentials.map((credentialRequest) => { -// const templateRecord = templatesById.get(credentialRequest.templateId)!; - -// // Normalize attributes shape and ensure it's valid -// const attributesMap = normalizeTemplateAttributes(templateRecord.attributes); - -// // ensure payload keys match known attributes -// const payloadKeys = Object.keys(credentialRequest.payload); -// const invalidPayloadKeys = payloadKeys.filter((payloadKey) => !attributesMap[payloadKey]); -// if (invalidPayloadKeys.length) { -// throw new Error( -// `Invalid attributes for template "${credentialRequest.templateId}": ${invalidPayloadKeys.join(', ')}` -// ); -// } - -// // Validate mandatory claims -// assertMandatoryClaims(credentialRequest.payload, attributesMap, { templateId: credentialRequest.templateId }); - -// const selectedApiFormat = mapDbFormatToApiFormat(templateRecord.format); -// const idSuffix = formatSuffix(selectedApiFormat); -// const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; - -// return { -// credentialSupportedId, -// format: selectedApiFormat, -// payload: credentialRequest.payload, -// ...(credentialRequest.disclosureFrame ? { disclosureFrame: credentialRequest.disclosureFrame } : {}) -// }; -// }); - -// return { -// credentials: normalizedCredentials -// }; -// } - export function buildCredentialOfferUrl(baseUrl: string, getAllCredentialOffer: GetAllCredentialOffer): string { const criteriaParams: string[] = []; @@ -613,7 +267,7 @@ function validateCredentialDatesInCertificateWindow(credentialValidityInfo: vali }; } -function buildSdJwtCredentialNew( +function buildSdJwtCredential( credentialRequest: CredentialRequestDtoLike, templateRecord: any, signerOptions: SignerOption[], @@ -675,7 +329,7 @@ function buildSdJwtCredentialNew( /** Build an MSO mdoc credential object * - For mdocs we expect the payload to include a `namespaces` map (draft-15 style) */ -function buildMdocCredentialNew( +function buildMdocCredential( credentialRequest: CredentialRequestDtoLike, templateRecord: any, signerOptions: SignerOption[], @@ -705,29 +359,6 @@ function buildMdocCredentialNew( validityInfo: credentialRequest.validityInfo }; - // // If caller provided already-namespaced payload, keep it; otherwise build a namespaces map - // const workingPayload = { ...incomingPayload }; - // if (!workingPayload.namespaces) { - // const namespacesMap: Record> = {}; - // // collect claims that match attribute names into the chosen namespace - // for (const claimName of Object.keys(normalizedAttributes)) { - // if (Object.prototype.hasOwnProperty.call(incomingPayload, claimName)) { - // namespacesMap[defaultNamespace] = namespacesMap[defaultNamespace] ?? {}; - // namespacesMap[defaultNamespace][claimName] = (incomingPayload as any)[claimName]; - // // remove original flattened claim to avoid duplication - // delete (workingPayload as any)[claimName]; - // } - // } - // if (0 < Object.keys(namespacesMap).length) { - // (workingPayload as any).namespaces = namespacesMap; - // } - // } else { - // // ensure namespaces is a plain object - // if (!isPlainRecord((workingPayload as any).namespaces)) { - // throw new Error(`Invalid mdoc payload: 'namespaces' must be an object`); - // } - // } - const apiFormat = mapDbFormatToApiFormat(templateRecord.format); const idSuffix = formatSuffix(apiFormat); const credentialSupportedId = `${templateRecord.name}-${idSuffix}`; @@ -740,7 +371,7 @@ function buildMdocCredentialNew( }; } -export function buildCredentialOfferPayloadNew( +export function buildCredentialOfferPayload( dto: CreateOidcCredentialOfferDtoLike, templates: credential_templates[], issuerDetails?: { @@ -771,10 +402,10 @@ export function buildCredentialOfferPayloadNew( const templateFormat = (templateRecord as any).format ?? 'vc+sd-jwt'; const apiFormat = mapDbFormatToApiFormat(templateFormat); if (apiFormat === CredentialFormat.SdJwtVc) { - return buildSdJwtCredentialNew(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); + return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); } if (apiFormat === CredentialFormat.Mdoc) { - return buildMdocCredentialNew(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); + return buildMdocCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); } throw new Error(`Unsupported template format for ${templateFormat}`); }); diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index ee4ae452c..a8555607a 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -112,130 +112,6 @@ type TemplateRowPrisma = { updatedAt?: Date | string; }; -/** - * Build agent payload from Prisma rows (attributes/appearance are Prisma.JsonValue). - * Safely coerces JSON and then builds the same structure as Builder #2. - */ -// export function buildCredentialConfigurationsSupported( -// templateRows: TemplateRowPrisma[], -// options?: { -// vct?: string; -// doctype?: string; -// scopeVct?: string; -// keyResolver?: (templateRow: TemplateRowPrisma) => string; -// format?: string; -// } -// ): Record { -// const defaultFormat = options?.format ?? 'vc+sd-jwt'; -// const credentialConfigMap: Record = {}; - -// for (const templateRow of templateRows) { -// // Extract and validate attributes (claims) and appearance (display configuration) -// const attributesJson = templateRow.attributes; -// const appearanceJson = coerceJsonObject(templateRow.appearance); - -// if (!isAttributesMap(attributesJson)) { -// throw new Error(`Template ${templateRow.id}: invalid attributes JSON`); -// } - -// if (!isAppearance(appearanceJson)) { -// throw new Error(`Template ${templateRow.id}: invalid appearance JSON (missing display array)`); -// } - -// // Determine credential format (either sd-jwt or mso_mdoc) -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// const templateFormat: string = (templateRow as any).format ?? defaultFormat; -// const isMdocFormat = 'mso_mdoc' === templateFormat; -// const formatSuffix = isMdocFormat ? 'mdoc' : 'sdjwt'; - -// // Determine the unique key for this credential configuration -// const configKey = -// 'function' === typeof options?.keyResolver -// ? options.keyResolver(templateRow) -// : `${templateRow.name}-${formatSuffix}`; - -// // Resolve Doctype and VCT based on format type -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// let resolvedDoctype: string | undefined = options?.doctype ?? (templateRow as any).doctype; -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// const resolvedVct: string = options?.vct ?? (templateRow as any).vct ?? templateRow.name; - -// if (isMdocFormat && !resolvedDoctype) { -// resolvedDoctype = templateRow.name; // fallback to template name -// } - -// // Construct OIDC4VC scope -// const scopeBaseValue = options?.scopeVct ?? (isMdocFormat ? resolvedDoctype : resolvedVct); -// const credentialScope = `openid4vc:credential:${scopeBaseValue}-${formatSuffix}`; - -// // Convert each attribute into a claim definition (map shape) -// const claimsObject: Record = {}; -// for (const [claimName, attributeDefinition] of Object.entries(attributesJson)) { -// console.log(`claimName, attributeDefinition`, claimName, attributeDefinition); -// console.log(`attributesJson`, attributesJson); -// const parsedAttribute = attributeDefinition as AttributeDef; - -// claimsObject[claimName] = { -// path: [claimName], -// mandatory: parsedAttribute.mandatory ?? false, -// display: Array.isArray(parsedAttribute.display) -// ? parsedAttribute.display.map((displayItem) => ({ -// name: displayItem.name, -// locale: displayItem.locale -// })) -// : undefined, -// value_type: parsedAttribute.value_type -// }; -// } - -// // Prepare the display configuration -// const displayConfigurations = -// (appearanceJson as 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 -// })) ?? []; - -// // Assemble final credential configuration -// credentialConfigMap[configKey] = { -// format: templateFormat, -// scope: credentialScope, -// // eslint-disable-next-line @typescript-eslint/no-unused-vars -// claims: Object.entries(claimsObject).map(([claimName, claimDef]) => ({ -// path: claimDef.path, -// mandatory: claimDef.mandatory, -// display: claimDef.display -// // you can optionally expose claimDef.value_type here if your API schema allows -// })), -// credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], -// cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], -// display: displayConfigurations, -// ...(isMdocFormat ? { doctype: resolvedDoctype as string } : { vct: resolvedVct }) -// }; -// } - -// return credentialConfigMap; // ✅ Return flat map, not nested object -// } - -/** - * Helper — Optional - * Wraps the credential configurations map into the expected schema - * for issuer metadata JSON: - * - * { "credentialConfigurationsSupported": { ... } } - */ -export function wrapCredentialConfigurationsSupported( - configsMap: Record -): CredentialConfigurationsSupported { - return { credentialConfigurationsSupported: configsMap }; -} - // Default DPoP list for issuer-level metadata (match your example) const ISSUER_DPOP_ALGS_DEFAULT = ['RS256', 'ES256'] as const; @@ -499,7 +375,7 @@ export function buildCredentialConfig(name: string, template: SdJwtTemplate | Md */ //TODO: Fix this eslint issue // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function buildCredentialConfigurationsSupportedNew(templateRows: any): Record { +export function buildCredentialConfigurationsSupported(templateRows: any): Record { const credentialConfigMap: Record = {}; for (const templateRow of templateRows) { diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index d877de735..368afd678 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -40,7 +40,7 @@ import { dpopSigningAlgValuesSupported } from '../constant/issuance'; import { - buildCredentialConfigurationsSupportedNew, + buildCredentialConfigurationsSupported, buildIssuerPayload, encodeIssuerPublicId, extractTemplateIds, @@ -53,8 +53,7 @@ import { UpdateCredentialRequest } from '../interfaces/oid4vc-issuer-sessions.interfaces'; import { - // buildCredentialOfferPayload, - buildCredentialOfferPayloadNew, + buildCredentialOfferPayload, buildCredentialOfferUrl, CredentialOfferPayload } from '../libs/helpers/credential-sessions.builder'; @@ -315,13 +314,7 @@ export class Oid4vcIssuanceService { if (!createdTemplate) { throw new InternalServerErrorException(ResponseMessages.oidcTemplate.error.createFailed); } - // let opts = {}; - // if (vct) { - // opts = { ...opts, vct }; - // } - // if (doctype) { - // opts = { ...opts, doctype }; - // } + let createTemplateOnAgent; try { const issuerTemplateConfig = await this.buildOidcIssuerConfig(issuerId); @@ -584,7 +577,7 @@ export class Oid4vcIssuanceService { //TODO: add logic to pass the issuer info const issuerDetailsFromDb = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); const { publicIssuerId, authorizationServerUrl } = issuerDetailsFromDb || {}; - const buildOidcCredentialOffer: CredentialOfferPayload = buildCredentialOfferPayloadNew( + const buildOidcCredentialOffer: CredentialOfferPayload = buildCredentialOfferPayload( createOidcCredentialOffer, // getAllOfferTemplates, @@ -767,7 +760,7 @@ export class Oid4vcIssuanceService { const issuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(issuerId); const templates = await this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); - const credentialConfigurationsSupported = buildCredentialConfigurationsSupportedNew(templates); + const credentialConfigurationsSupported = buildCredentialConfigurationsSupported(templates); return buildIssuerPayload({ credentialConfigurationsSupported }, issuerDetails); } catch (error) { From 4e3ec43d61f44cb0a00e312ed04c2b251bc8af60 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Wed, 29 Oct 2025 19:31:58 +0530 Subject: [PATCH 20/43] fix: resolved coderabbit suggested chnages Signed-off-by: Rinkal Bhojani --- .../agent-service/dto/create-schema.dto.ts | 2 ++ .../dtos/issuer-sessions.dto.ts | 9 ++++--- .../dtos/oid4vc-issuer-template.dto.ts | 18 ++++---------- .../interfaces/oid4vc-template.interfaces.ts | 4 ++-- libs/common/src/date-only.ts | 24 +++++++++++++++++-- libs/enum/src/enum.ts | 10 ++++++++ 6 files changed, 47 insertions(+), 20 deletions(-) diff --git a/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts index 21dd0cf1b..e8a8a1a0c 100644 --- a/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts +++ b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts @@ -21,6 +21,8 @@ export class CreateTenantSchemaDto { @IsArray({ message: 'attributes must be an array' }) @IsString({ each: true }) // TODO: IsNotEmpty won't work for array. Must use @ArrayNotEmpty() instead + // @ArrayNotEmpty({ message: 'please provide at least one attribute' }) + // @IsNotEmpty({ each: true, message: 'attribute must not be empty' }) @IsNotEmpty({ message: 'please provide valid attributes' }) attributes: string[]; 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 2325de600..fc2996c6f 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 @@ -17,7 +17,8 @@ import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, - Validate + Validate, + IsDate } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; @@ -123,16 +124,18 @@ export class ValidityInfo { example: '2025-04-23T14:34:09.188Z', required: true }) - @IsString() @IsNotEmpty() + @Type(() => Date) + @IsDate() validFrom: Date; @ApiProperty({ example: '2026-05-03T14:34:09.188Z', required: true }) - @IsString() @IsNotEmpty() + @Type(() => Date) + @IsDate() validUntil: Date; } 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 13a6a98e6..5397d6524 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 @@ -15,7 +15,7 @@ import { import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath, PartialType } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { SignerOption } from '@prisma/client'; -import { CredentialFormat } from '@credebl/enum/enum'; +import { AttributeType, CredentialFormat } from '@credebl/enum/enum'; class CredentialAttributeDisplayDto { @ApiPropertyOptional({ example: 'First Name' }) @@ -28,17 +28,6 @@ class CredentialAttributeDisplayDto { @IsOptional() locale?: string; } - -export enum AttributeType { - STRING = 'string', - NUMBER = 'number', - BOOLEAN = 'boolean', - DATE = 'date', - OBJECT = 'object', - ARRAY = 'array', - IMAGE = 'image' -} - export class CredentialAttributeDto { @ApiProperty({ description: 'Unique key for this attribute (e.g., full_name, org.iso.23220.photoID.1.birth_date)' }) @IsString() @@ -52,7 +41,8 @@ export class CredentialAttributeDto { // TODO: Check how do we handle claims with only path rpoperty like email, etc. @ApiProperty({ enum: AttributeType, description: 'Type of the attribute value (string, number, date, etc.)' }) @IsEnum(AttributeType) - value_type: string; + // TODO: changes value_type: AttributeType; + value_type: AttributeType; @ApiProperty({ description: 'Whether this attribute should be disclosed (for SD-JWT)' }) @IsOptional() @@ -210,6 +200,8 @@ export class SdJwtTemplateDto { description: 'Attributes included in the credential template' }) @IsArray() + @ValidateNested({ each: true }) + @Type(() => CredentialAttributeDto) attributes: CredentialAttributeDto[]; } diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts index 5f590389c..2b33c19e1 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces.ts @@ -1,5 +1,5 @@ import { Prisma, SignerOption } from '@prisma/client'; -import { CredentialFormat } from '@credebl/enum/enum'; +import { AttributeType, CredentialFormat } from '@credebl/enum/enum'; export interface SdJwtTemplate { vct: string; attributes: CredentialAttribute[]; @@ -40,7 +40,7 @@ export interface Claim { export interface CredentialAttribute { key: string; mandatory?: boolean; - value_type: string; + value_type: AttributeType; disclose?: boolean; children?: CredentialAttribute[]; display?: ClaimDisplay[]; diff --git a/libs/common/src/date-only.ts b/libs/common/src/date-only.ts index 969a4ce2f..5cd199339 100644 --- a/libs/common/src/date-only.ts +++ b/libs/common/src/date-only.ts @@ -2,8 +2,25 @@ const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); export class DateOnly { private date: Date; - public constructor(date?: string) { - this.date = date ? new Date(date) : new Date(); + public constructor(date?: string | Date) { + if (date instanceof Date) { + if (isNaN(date.getTime())) { + throw new TypeError('Invalid Date'); + } + this.date = date; + return; + } + if (!date) { + this.date = new Date(); + return; + } + // Accept only YYYY-MM-DD or full ISO strings + const iso = /^\d{4}-\d{2}-\d{2}(T.*Z)?$/.test(date) ? date : ''; + const d = new Date(iso || date); + if (isNaN(d.getTime())) { + throw new TypeError('Invalid date string'); + } + this.date = d; } get [Symbol.toStringTag](): string { @@ -34,5 +51,8 @@ export const serverStartupTimeInMilliseconds = Date.now(); export function dateToSeconds(date: Date | DateOnly): number { const realDate = date instanceof DateOnly ? new Date(date.toISOString()) : date; + if (isNaN(realDate.getTime())) { + throw new TypeError('dateToSeconds: invalid date'); + } return Math.floor(realDate.getTime() / 1000); } diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 043ca4e1f..59586a71b 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -323,6 +323,16 @@ export enum CredentialFormat { Mdoc = 'mso_mdoc' } +export enum AttributeType { + STRING = 'string', + NUMBER = 'number', + BOOLEAN = 'boolean', + DATE = 'date', + OBJECT = 'object', + ARRAY = 'array', + IMAGE = 'image' +} + // export enum SignerOption { // DID, // X509_P256, From 6a66cf444d6050076964919d5848c1f99d532673 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Thu, 30 Oct 2025 11:42:58 +0530 Subject: [PATCH 21/43] feat: added webhook Signed-off-by: Tipu_Singh --- .../dtos/oid4vc-credential-wh.dto.ts | 50 +++-- .../oid4vc-issuance.controller.ts | 4 +- .../interfaces/oid4vc-wh-interfaces.ts | 44 +---- .../src/oid4vc-issuance.controller.ts | 4 +- .../src/oid4vc-issuance.repository.ts | 42 ++++- .../src/oid4vc-issuance.service.ts | 62 ++++-- .../migration.sql | 22 +++ libs/prisma-service/prisma/schema.prisma | 176 +++++++++--------- 8 files changed, 240 insertions(+), 164 deletions(-) create mode 100644 libs/prisma-service/prisma/migrations/20251029164125_updated_table_oid4vc_credentials/migration.sql diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts index d4a02f669..54e4a8406 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts @@ -2,23 +2,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsArray, IsObject, IsString } from 'class-validator'; export class CredentialOfferPayloadDto { - @ApiProperty() - @IsString() - // eslint-disable-next-line camelcase - credential_issuer!: string; - @ApiProperty({ type: [String] }) @IsArray() // eslint-disable-next-line camelcase credential_configuration_ids!: string[]; - - @ApiProperty({ type: 'object', additionalProperties: true }) - @IsObject() - grants!: Record; - - @ApiProperty({ type: [Object] }) - @IsArray() - credentials!: Record[]; } export class IssuanceMetadataDto { @@ -40,11 +27,48 @@ export class OidcIssueCredentialDto { @IsString() credentialOfferId!: string; + @ApiProperty({ type: [Object] }) + @IsArray() + issuedCredentials!: Record[]; + + @ApiProperty({ type: CredentialOfferPayloadDto }) + @IsObject() + credentialOfferPayload!: CredentialOfferPayloadDto; + @ApiProperty() @IsString() state!: string; + @ApiProperty() + @IsString() + createdAt!: string; + + @ApiProperty() + @IsString() + updatedAt!: string; + @ApiProperty() @IsString() contextCorrelationId!: string; } + +/** + * Utility: return only credential_configuration_ids from a webhook payload + */ +export function extractCredentialConfigurationIds(payload: Partial): string[] { + const cfg = payload?.credentialOfferPayload?.credential_configuration_ids; + return Array.isArray(cfg) ? cfg : []; +} + +export function sanitizeOidcIssueCredentialDto( + payload: Partial +): Partial { + const ids = extractCredentialConfigurationIds(payload); + return { + ...payload, + credentialOfferPayload: { + // eslint-disable-next-line camelcase + credential_configuration_ids: ids + } + }; +} diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts index 66b050146..b7ea3892b 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts @@ -45,7 +45,7 @@ import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler import { user } from '@prisma/client'; import { IssuerCreationDto, IssuerUpdationDto } from './dtos/oid4vc-issuer.dto'; import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oid4vc-issuer-template.dto'; -import { OidcIssueCredentialDto } from './dtos/oid4vc-credential-wh.dto'; +import { OidcIssueCredentialDto, sanitizeOidcIssueCredentialDto } from './dtos/oid4vc-credential-wh.dto'; import { Oid4vcIssuanceService } from './oid4vc-issuance.service'; import { CreateCredentialOfferD2ADto, @@ -630,7 +630,7 @@ export class Oid4vcIssuanceController { @Param('id') id: string, @Res() res: Response ): Promise { - console.log('Webhook received:', JSON.stringify(oidcIssueCredentialDto, null, 2)); + // const sanitized = sanitizeOidcIssueCredentialDto(oidcIssueCredentialDto); const getCredentialDetails = await this.oid4vcIssuanceService.oidcIssueCredentialWebhook( oidcIssueCredentialDto, id diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts index 3f878528e..1e48e4c0f 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts @@ -1,38 +1,14 @@ -export interface CredentialOfferPayload { - credential_issuer: string; - credential_configuration_ids: string[]; - grants: Record; - credentials: Record[]; -} - -export interface IssuanceMetadata { - issuerDid: string; - credentials: Record[]; -} - -export interface OidcIssueCredential { - _tags: Record; - metadata: Record; - issuedCredentials: Record[]; - id: string; - createdAt: string; // ISO date string - issuerId: string; - userPin: string; - preAuthorizedCode: string; - credentialOfferUri: string; - credentialOfferId: string; - credentialOfferPayload: CredentialOfferPayload; - issuanceMetadata: IssuanceMetadata; - state: string; - updatedAt: string; // ISO date string - contextCorrelationId: string; -} - -export interface CredentialOfferWebhookPayload { - credentialOfferId: string; +export interface Oid4vcCredentialOfferWebhookPayload { id: string; - State: string; - contextCorrelationId: string; + credentialOfferId?: string; + issuedCredentials?: Record[]; + createdAt?: string; + updatedAt?: string; + credentialOfferPayload?: { + credential_configuration_ids?: string[]; + }; + state?: string; + contextCorrelationId?: string; } export interface CredentialPayload { diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts index 1812e3c90..01b69f5d6 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.controller.ts @@ -10,7 +10,7 @@ import { GetAllCredentialOffer } from '../interfaces/oid4vc-issuer-sessions.interfaces'; import { CreateCredentialTemplate, UpdateCredentialTemplate } from '../interfaces/oid4vc-template.interfaces'; -import { CredentialOfferWebhookPayload } from '../interfaces/oid4vc-wh-interfaces'; +import { Oid4vcCredentialOfferWebhookPayload } from '../interfaces/oid4vc-wh-interfaces'; @Controller() export class Oid4vcIssuanceController { @@ -176,7 +176,7 @@ export class Oid4vcIssuanceController { } @MessagePattern({ cmd: 'webhook-oid4vc-issue-credential' }) - async oidcIssueCredentialWebhook(payload: CredentialOfferWebhookPayload): Promise { + async oidcIssueCredentialWebhook(payload: Oid4vcCredentialOfferWebhookPayload): Promise { return this.oid4vcIssuanceService.storeOidcCredentialWebhook(payload); } } diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts index d7651172b..959cccb56 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts @@ -65,18 +65,40 @@ export class Oid4vcIssuanceRepository { } } - async storeOidcCredentialDetails(credentialPayload): Promise { + async storeOidcCredentialDetails( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + credentialPayload: + | { + id: string; + credentialOfferId?: string; + state?: string; + contextCorrelationId?: string; + credentialConfigurationIds?: string[]; + issuedCredentials?: string[]; + } + | any, + orgId: string + ): Promise { try { - const { credentialOfferId, state, offerId, contextCorrelationId, orgId } = credentialPayload; + const payload = credentialPayload?.oidcIssueCredentialDto ?? credentialPayload ?? {}; + const { + credentialOfferId, + state, + id: issuanceSessionId, + contextCorrelationId, + credentialOfferPayload, + issuedCredentials + } = payload; + const credentialDetails = await this.prisma.oid4vc_credentials.upsert({ where: { - offerId + issuanceSessionId }, update: { lastChangedBy: orgId, - credentialOfferId, - contextCorrelationId, - state + state, + credentialConfigurationIds: credentialOfferPayload.credential_configuration_ids ?? [], + ...(issuedCredentials !== undefined ? { issuedCredentials } : {}) }, create: { lastChangedBy: orgId, @@ -84,14 +106,16 @@ export class Oid4vcIssuanceRepository { state, orgId, credentialOfferId, - offerId, - contextCorrelationId + contextCorrelationId, + issuanceSessionId, + credentialConfigurationIds: credentialOfferPayload.credential_configuration_ids ?? [], + ...(issuedCredentials !== undefined ? { issuedCredentials } : {}) } }); return credentialDetails; } catch (error) { - this.logger.error(`Error in get storeOidcCredentialDetails: ${error.message} `); + this.logger.error(`Error in storeOidcCredentialDetails in issuance repository: ${error.message} `); throw error; } } diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 368afd678..921575f02 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -60,6 +60,7 @@ import { import { x5cKeyType } from '@credebl/enum/enum'; import { instanceToPlain, plainToInstance } from 'class-transformer'; import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; +import { Oid4vcCredentialOfferWebhookPayload } from '../interfaces/oid4vc-wh-interfaces'; type CredentialDisplayItem = { logo?: { uri: string; alt_text?: string }; @@ -953,33 +954,60 @@ export class Oid4vcIssuanceService { return agentDetails.agentEndPoint; } - async storeOidcCredentialWebhook(CredentialOfferWebhookPayload): Promise { + async storeOidcCredentialWebhook( + CredentialOfferWebhookPayload: Oid4vcCredentialOfferWebhookPayload + ): Promise { try { - console.log('Storing OID4VC Credential Webhook:', CredentialOfferWebhookPayload); - const { credentialOfferId, state, id, contextCorrelationId } = CredentialOfferWebhookPayload; + // pick fields + const { + credentialOfferId, + state, + id: issuanceSessionId, + contextCorrelationId, + credentialOfferPayload, + issuedCredentials + } = CredentialOfferWebhookPayload ?? {}; + + // ensure we only store credential_configuration_ids in the payload for logging and storage + const cfgIds: string[] = Array.isArray(credentialOfferPayload?.credential_configuration_ids) + ? credentialOfferPayload.credential_configuration_ids + : []; + + // convert issuedCredentials to string[] when schema expects string[] + const issuedCredentialsArr: string[] | undefined = + Array.isArray(issuedCredentials) && 0 < issuedCredentials.length + ? issuedCredentials.map((c: any) => ('string' === typeof c ? c : JSON.stringify(c))) + : issuedCredentials && Array.isArray(issuedCredentials) && 0 === issuedCredentials.length + ? [] + : undefined; + + const sanitized = { + ...CredentialOfferWebhookPayload, + credentialOfferPayload: { + credential_configuration_ids: cfgIds + } + }; + + console.log('Storing OID4VC Credential Webhook:', JSON.stringify(sanitized, null, 2)); + + // resolve orgId (unchanged logic) let orgId: string; if ('default' !== contextCorrelationId) { const getOrganizationId = await this.oid4vcIssuanceRepository.getOrganizationByTenantId(contextCorrelationId); orgId = getOrganizationId?.orgId; } else { - orgId = id; + orgId = issuanceSessionId; } - const credentialPayload = { - orgId, - offerId: id, - credentialOfferId, - state, - contextCorrelationId - }; - - const agentDetails = await this.oid4vcIssuanceRepository.storeOidcCredentialDetails(credentialPayload); + // hand off to repository for persistence (repository will perform the upsert) + const agentDetails = await this.oid4vcIssuanceRepository.storeOidcCredentialDetails( + CredentialOfferWebhookPayload, + orgId + ); return agentDetails; } catch (error) { - this.logger.error( - `[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}` - ); - throw new RpcException(error.response ? error.response : error); + this.logger.error(`[storeOidcCredentialWebhook] - error: ${JSON.stringify(error)}`); + throw error; } } } diff --git a/libs/prisma-service/prisma/migrations/20251029164125_updated_table_oid4vc_credentials/migration.sql b/libs/prisma-service/prisma/migrations/20251029164125_updated_table_oid4vc_credentials/migration.sql new file mode 100644 index 000000000..c87bd9258 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251029164125_updated_table_oid4vc_credentials/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - You are about to drop the column `offerId` on the `oid4vc_credentials` table. All the data in the column will be lost. + - A unique constraint covering the columns `[issuanceSessionId]` on the table `oid4vc_credentials` will be added. If there are existing duplicate values, this will fail. + - Added the required column `issuanceSessionId` to the `oid4vc_credentials` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "oid4vc_credentials_offerId_key"; + +-- AlterTable +ALTER TABLE "oid4vc_credentials" DROP COLUMN "offerId", +ADD COLUMN "credentialConfigurationIds" TEXT[], +ADD COLUMN "issuanceSessionId" TEXT NOT NULL, +ADD COLUMN "issuedCredentials" TEXT[]; + +-- CreateIndex +CREATE UNIQUE INDEX "oid4vc_credentials_issuanceSessionId_key" ON "oid4vc_credentials"("issuanceSessionId"); + +-- CreateIndex +CREATE INDEX "oid4vc_credentials_credentialConfigurationIds_idx" ON "oid4vc_credentials" USING GIN ("credentialConfigurationIds"); diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index eebca4da4..5d3408b53 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -39,35 +39,35 @@ model user { } model account { - id String @id @default(uuid()) @db.Uuid - userId String @unique @db.Uuid + id String @id @default(uuid()) @db.Uuid + userId String @unique @db.Uuid type String? provider String - providerAccountId String + providerAccountId String tokenType String? scope String? idToken String? sessionState String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user user @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user user @relation(fields: [userId], references: [id]) sessions session[] - } +} model session { - id String @id @default(uuid()) @db.Uuid - sessionToken String - userId String @db.Uuid - expires Int - refreshToken String? - user user @relation(fields: [userId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - accountId String? @db.Uuid - sessionType String? - account account? @relation(fields: [accountId], references:[id]) - expiresAt DateTime? @db.Timestamp(6) - clientInfo Json? + id String @id @default(uuid()) @db.Uuid + sessionToken String + userId String @db.Uuid + expires Int + refreshToken String? + user user @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + accountId String? @db.Uuid + sessionType String? + account account? @relation(fields: [accountId], references: [id]) + expiresAt DateTime? @db.Timestamp(6) + clientInfo Json? } model token { @@ -151,7 +151,7 @@ model organisation { agent_invitations agent_invitations[] credential_definition credential_definition[] file_upload file_upload[] - oid4vc_credentials oid4vc_credentials[] + oid4vc_credentials oid4vc_credentials[] } model org_invitations { @@ -231,7 +231,7 @@ model org_agents { webhookUrl String? @db.VarChar org_dids org_dids[] oidc_issuer oidc_issuer[] - x509_certificates x509_certificates[] + x509_certificates x509_certificates[] } model org_dids { @@ -307,7 +307,7 @@ model schema { type String? @db.VarChar isSchemaArchived Boolean @default(false) credential_definition credential_definition[] - alias String? + alias String? } model credential_definition { @@ -353,16 +353,16 @@ model agent_invitations { } model connections { - id String @id @default(uuid()) @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedBy String @db.Uuid - connectionId String @unique - theirLabel String @default("") + id String @id @default(uuid()) @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + connectionId String @unique + theirLabel String @default("") state String - orgId String? @db.Uuid - organisation organisation? @relation(fields: [orgId], references: [id]) + orgId String? @db.Uuid + organisation organisation? @relation(fields: [orgId], references: [id]) presentations presentations[] credentials credentials[] } @@ -390,7 +390,7 @@ model presentations { createdBy String @db.Uuid lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) lastChangedBy String @db.Uuid - connectionId String? + connectionId String? state String? threadId String @unique isVerified Boolean? @@ -401,7 +401,6 @@ model presentations { orgId String? @db.Uuid organisation organisation? @relation(fields: [orgId], references: [id]) connections connections? @relation(fields: [connectionId], references: [connectionId]) - } model file_upload { @@ -418,7 +417,7 @@ model file_upload { organisation organisation? @relation(fields: [orgId], references: [id]) orgId String? @db.Uuid credential_type String? - templateId String? @db.VarChar + templateId String? @db.VarChar } model file_data { @@ -551,12 +550,12 @@ model cloud_wallet_user_info { createdBy String @db.Uuid lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) lastChangedBy String @db.Uuid - userId String? @db.Uuid + userId String? @db.Uuid agentEndpoint String? agentApiKey String? key String? connectionImageUrl String? - user user? @relation(fields: [userId], references: [id]) + user user? @relation(fields: [userId], references: [id]) } enum CloudWalletType { @@ -565,40 +564,45 @@ enum CloudWalletType { } model client_aliases { - id String @id @default(uuid()) @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) - clientAlias String? - clientUrl String + id String @id @default(uuid()) @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + clientAlias String? + clientUrl String } model oidc_issuer { - id String @id @default(uuid()) @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - publicIssuerId String - metadata Json - authorizationServerUrl String - orgAgentId String @db.Uuid - orgAgent org_agents @relation(fields: [orgAgentId], references: [id]) - templates credential_templates[] - batchCredentialIssuanceSize Int @default(0) + id String @id @default(uuid()) @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + publicIssuerId String + metadata Json + authorizationServerUrl String + orgAgentId String @db.Uuid + orgAgent org_agents @relation(fields: [orgAgentId], references: [id]) + templates credential_templates[] + batchCredentialIssuanceSize Int @default(0) + @@index([orgAgentId]) } model oid4vc_credentials { - id String @id @default(uuid()) @db.Uuid - orgId String @db.Uuid - offerId String @unique + id String @id @default(uuid()) @db.Uuid + orgId String @db.Uuid + issuanceSessionId String @unique credentialOfferId String state String contextCorrelationId String - createdBy String @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedBy String @db.Uuid + createdBy String @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + credentialConfigurationIds String[] + issuedCredentials String[] + + organisation organisation @relation(fields: [orgId], references: [id]) - organisation organisation @relation(fields: [orgId], references: [id]) + @@index([credentialConfigurationIds], type: Gin) } enum SignerOption { @@ -608,41 +612,39 @@ enum SignerOption { } model credential_templates { - id String @id @default(uuid()) - name String - description String? - format String // e.g. "sd_jwt", "mso_mdoc" - canBeRevoked Boolean @default(false) + id String @id @default(uuid()) + name String + description String? + format String // e.g. "sd_jwt", "mso_mdoc" + canBeRevoked Boolean @default(false) - attributes Json - appearance Json + attributes Json + appearance Json - issuerId String @db.Uuid - issuer oidc_issuer @relation(fields: [issuerId], references: [id]) + issuerId String @db.Uuid + issuer oidc_issuer @relation(fields: [issuerId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - signerOption SignerOption + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + signerOption SignerOption } model x509_certificates { - id String @id @default(uuid()) + id String @id @default(uuid()) - orgAgentId String @db.Uuid - org_agents org_agents @relation(fields: [orgAgentId], references: [id]) + orgAgentId String @db.Uuid + org_agents org_agents @relation(fields: [orgAgentId], references: [id]) - keyType String // "p256", "ed25519" - status String //e.g "Active", "Pending activation", "InActive" - validFrom DateTime + keyType String // "p256", "ed25519" + status String //e.g "Active", "Pending activation", "InActive" + validFrom DateTime - expiry DateTime - certificateBase64 String - isImported Boolean @default(false) + expiry DateTime + certificateBase64 String + isImported Boolean @default(false) - createdAt DateTime @default(now()) - createdBy String @db.Uuid - lastChangedDateTime DateTime @updatedAt - lastChangedBy String @db.Uuid + createdAt DateTime @default(now()) + createdBy String @db.Uuid + lastChangedDateTime DateTime @updatedAt + lastChangedBy String @db.Uuid } - - From 4174168fcdeb9ca9b748a87cfd4e8e2cd9b55476 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Thu, 30 Oct 2025 16:24:47 +0530 Subject: [PATCH 22/43] fix: fixed issue with signeroption did in sdjwt create offer Signed-off-by: Rinkal Bhojani --- .../libs/helpers/credential-sessions.builder.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index f473c827c..fa73b96a1 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -279,8 +279,16 @@ function buildSdJwtCredential( // // strip vct if present per requirement // delete payloadCopy.vct; - if (signerOptions[0].method === SignerMethodOption.X5C && credentialRequest.validityInfo) { - const certificateDetail = activeCertificateDetails.find((x) => x.certificateBase64 === signerOptions[0].x5c[0]); + let templateSignerOption: SignerOption; + if (templateRecord.signerOption.toLowerCase() === SignerMethodOption.X5C) { + templateSignerOption = signerOptions.find((x) => SignerMethodOption.X5C === x.method); + } else if (templateRecord.signerOption.toLowerCase() === SignerMethodOption.DID) { + templateSignerOption = signerOptions.find((x) => SignerMethodOption.DID === x.method); + } + + if (templateRecord.signerOption === SignerMethodOption.X5C && credentialRequest.validityInfo) { + const certificateDetail = activeCertificateDetails.find((x) => x.certificateBase64 === templateSignerOption.x5c[0]); + const validationResult = validateCredentialDatesInCertificateWindow( credentialRequest.validityInfo, certificateDetail @@ -319,7 +327,7 @@ function buildSdJwtCredential( return { credentialSupportedId, - signerOptions: signerOptions ? signerOptions[0] : undefined, + signerOptions: templateSignerOption ? templateSignerOption : undefined, format: apiFormat, payload: payloadCopy, ...(disclosureFrame ? { disclosureFrame } : {}) From 407d6498f3f31b72867467f12a438bfce06d86c5 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Thu, 30 Oct 2025 22:08:00 +0530 Subject: [PATCH 23/43] fix: Reverted accidental changes done in credebl-master-table.json file, fixed other values Signed-off-by: Rinkal Bhojani --- .../src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts | 10 +++++----- .../libs/helpers/credential-sessions.builder.ts | 9 +++------ .../prisma/data/credebl-master-table.json | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts index c0c340d38..2675d7514 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -108,7 +108,7 @@ export class IssuerCreationDto { name: 'Credebl University', description: 'Accredited institution issuing verified student credentials', logo: { - uri: 'https://university.credebl.io/assets/logo-en.svg', + uri: 'https://university.example.io/assets/logo-en.svg', alt_text: 'Credebl University logo' } }, @@ -117,7 +117,7 @@ export class IssuerCreationDto { name: 'Credebl Universität', description: 'Akkreditierte Institution für digitale Studentenausweise', logo: { - uri: 'https://university.credebl.io/assets/logo-de.svg', + uri: 'https://university.example.io/assets/logo-de.svg', alt_text: 'Credebl Universität Logo' } } @@ -129,7 +129,7 @@ export class IssuerCreationDto { display: IssuerDisplayDto[]; @ApiProperty({ - example: 'https://issuer.credebl.io', + example: 'https://issuer.example.io', description: 'Base URL of the Authorization Server supporting OID4VC issuance flows' }) @IsUrl({ require_tld: false }, { message: 'authorizationServerUrl must be a valid URL' }) @@ -140,10 +140,10 @@ export class IssuerCreationDto { 'Additional configuration details for the authorization server (token endpoint, credential endpoint, grant types, etc.)', type: AuthorizationServerConfigDto, example: { - issuer: 'https://id.sovio.ae:8443/realms/sovioid', + issuer: 'https://example.com/realms/abc', clientAuthentication: { clientId: 'issuer-server', - clientSecret: '1qKMWulZpMBzXIdfPO5AEs0xaTaKs1ym' + clientSecret: 'issuer-client-secret' } } }) diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index fa73b96a1..089052c44 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -279,12 +279,9 @@ function buildSdJwtCredential( // // strip vct if present per requirement // delete payloadCopy.vct; - let templateSignerOption: SignerOption; - if (templateRecord.signerOption.toLowerCase() === SignerMethodOption.X5C) { - templateSignerOption = signerOptions.find((x) => SignerMethodOption.X5C === x.method); - } else if (templateRecord.signerOption.toLowerCase() === SignerMethodOption.DID) { - templateSignerOption = signerOptions.find((x) => SignerMethodOption.DID === x.method); - } + const templateSignerOption: SignerOption = signerOptions.find( + (x) => templateRecord.signerOption.toLowerCase() === x.method + ); if (templateRecord.signerOption === SignerMethodOption.X5C && credentialRequest.validityInfo) { const certificateDetail = activeCertificateDetails.find((x) => x.certificateBase64 === templateSignerOption.x5c[0]); diff --git a/libs/prisma-service/prisma/data/credebl-master-table.json b/libs/prisma-service/prisma/data/credebl-master-table.json index f20abc6a9..c4167aa18 100644 --- a/libs/prisma-service/prisma/data/credebl-master-table.json +++ b/libs/prisma-service/prisma/data/credebl-master-table.json @@ -1,5 +1,5 @@ { - "platformConfigData": { + "platformConfigData": { "externalIp": "##Machine Ip Address/Domain for agent setup##", "inboundEndpoint": "##Machine Ip Address/Domain for agent setup##", "username": "credebl", From 76c3b70ed3af0d967d1547d4e22ac6dc6d2b6807 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Mon, 3 Nov 2025 13:07:24 +0530 Subject: [PATCH 24/43] feat: oid4vc verification (#1501) * feat:oid4vc verifications Signed-off-by: Tipu_Singh * feat: CRU for verifier Signed-off-by: Krishna Waske * fix: get verifier api Signed-off-by: Krishna Waske * feat: add delete verifier Signed-off-by: Krishna Waske * fix: delete verifier reselient Signed-off-by: Krishna Waske * feat: oid4vc verification verifier session (#1502) * fix: verification session Signed-off-by: Krishna Waske * feat: add verifier session response by id Signed-off-by: Krishna Waske --------- Signed-off-by: Krishna Waske * feat: verifier session post API (#1503) * feat: verifier session post API Signed-off-by: Tipu_Singh * fix: delete oid4vc verifier Signed-off-by: Krishna Waske * fix: change DTO example Signed-off-by: Krishna Waske * fix: change CREDEBL to example Signed-off-by: Krishna Waske * fix: session to presentation Signed-off-by: Krishna Waske * feat: added docker file for verification Signed-off-by: Tipu_Singh * fix: remove unnecessary column for oid4vp_verifier table Signed-off-by: Krishna Waske --------- Signed-off-by: Tipu_Singh Signed-off-by: Krishna Waske Co-authored-by: Krishna Waske --------- Signed-off-by: Tipu_Singh Signed-off-by: Krishna Waske Co-authored-by: Tipu_Singh --- Dockerfiles/Dockerfile.oid4vc-verification | 45 +++ .../src/agent-service.controller.ts | 38 ++ .../src/agent-service.service.ts | 63 +++ apps/api-gateway/src/app.module.ts | 2 + .../dtos/oid4vc-verifier-presentation.dto.ts | 350 +++++++++++++++++ .../dtos/oid4vc-verifier.dto.ts | 44 +++ .../oid4vc-verification.controller.ts | 370 +++++++++++++++++ .../oid4vc-verification.module.ts | 24 ++ .../oid4vc-verification.service.ts | 69 ++++ .../interfaces/oid4vp-verifier.interfaces.ts | 27 ++ apps/oid4vc-verification/src/main.ts | 23 ++ .../src/oid4vc-verification.controller.ts | 73 ++++ .../src/oid4vc-verification.module.ts | 39 ++ .../src/oid4vc-verification.repository.ts | 163 ++++++++ .../src/oid4vc-verification.service.ts | 371 ++++++++++++++++++ apps/oid4vc-verification/test/app.e2e-spec.ts | 19 + apps/oid4vc-verification/test/jest-e2e.json | 9 + apps/oid4vc-verification/tsconfig.app.json | 9 + libs/common/src/cast.helper.ts | 17 + libs/common/src/common.constant.ts | 20 + libs/common/src/common.utils.ts | 20 +- .../src/interfaces/oid4vp-verification.ts | 33 ++ libs/common/src/response-messages/index.ts | 20 + .../migration.sql | 24 ++ .../migration.sql | 8 + libs/prisma-service/prisma/schema.prisma | 15 + nest-cli.json | 287 +++++++------- tsconfig.json | 108 ++--- 28 files changed, 2095 insertions(+), 195 deletions(-) create mode 100644 Dockerfiles/Dockerfile.oid4vc-verification create mode 100644 apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts create mode 100644 apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier.dto.ts create mode 100644 apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts create mode 100644 apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts create mode 100644 apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts create mode 100644 apps/oid4vc-verification/interfaces/oid4vp-verifier.interfaces.ts create mode 100644 apps/oid4vc-verification/src/main.ts create mode 100644 apps/oid4vc-verification/src/oid4vc-verification.controller.ts create mode 100644 apps/oid4vc-verification/src/oid4vc-verification.module.ts create mode 100644 apps/oid4vc-verification/src/oid4vc-verification.repository.ts create mode 100644 apps/oid4vc-verification/src/oid4vc-verification.service.ts create mode 100644 apps/oid4vc-verification/test/app.e2e-spec.ts create mode 100644 apps/oid4vc-verification/test/jest-e2e.json create mode 100644 apps/oid4vc-verification/tsconfig.app.json create mode 100644 libs/common/src/interfaces/oid4vp-verification.ts create mode 100644 libs/prisma-service/prisma/migrations/20251016105943_added_new_oid4vp_verifier_table/migration.sql create mode 100644 libs/prisma-service/prisma/migrations/20251103071640_updated_table_oid4vp_verifier_to_remove_unnecessary_column_deleted_at/migration.sql diff --git a/Dockerfiles/Dockerfile.oid4vc-verification b/Dockerfiles/Dockerfile.oid4vc-verification new file mode 100644 index 000000000..97c13e087 --- /dev/null +++ b/Dockerfiles/Dockerfile.oid4vc-verification @@ -0,0 +1,45 @@ +# Stage 1: Build the application +FROM node:18-alpine as build +# Install OpenSSL +RUN apk add --no-cache openssl +RUN npm install -g pnpm +# Set the working directory +WORKDIR /app + +# Copy package.json and package-lock.json +COPY package.json ./ +COPY pnpm-workspace.yaml ./ +#COPY package-lock.json ./ + +ENV PUPPETEER_SKIP_DOWNLOAD=true + +# Install dependencies while ignoring scripts (including Puppeteer's installation) +RUN pnpm i --ignore-scripts + +# Copy the rest of the application code +COPY . . +# RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate +RUN cd libs/prisma-service && npx prisma generate + +# Build the oid4vc-verification service +RUN npm run build oid4vc-verification + + +# Stage 2: Create the final image +FROM node:18-alpine +# Install OpenSSL +RUN apk add --no-cache openssl +# RUN npm install -g pnpm +# Set the working directory +WORKDIR /app + +# Copy the compiled code from the build stage +COPY --from=build /app/dist/apps/oid4vc-verification/ ./dist/apps/oid4vc-verification/ + +# Copy the libs folder from the build stage +COPY --from=build /app/libs/ ./libs/ +#COPY --from=build /app/package.json ./ +COPY --from=build /app/node_modules ./node_modules + +# Set the command to run the microservice +CMD ["sh", "-c", "cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate && cd ../.. && node dist/apps/oid4vc-verification/main.js"] \ No newline at end of file diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index e0f82ce20..1e3a914ca 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -32,6 +32,7 @@ import { x509CertificateDecodeDto, X509CreateCertificateOptions } from '@credebl/common/interfaces/x509.interface'; +import { CreateVerifier, UpdateVerifier } from '@credebl/common/interfaces/oid4vp-verification'; @Controller() export class AgentServiceController { @@ -412,4 +413,41 @@ export class AgentServiceController { }): Promise { return this.agentServiceService.importX509Certificate(payload.options, payload.url, payload.orgId); } + + @MessagePattern({ cmd: 'agent-create-oid4vp-verifier' }) + async createOid4vpVerifier(payload: { + verifierDetails: CreateVerifier; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.createOid4vpVerifier(payload.verifierDetails, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-delete-oid4vp-verifier' }) + async deleteOid4vpVerifier(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.deleteOid4vpVerifier(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-update-oid4vp-verifier' }) + async updateOid4vpVerifier(payload: { + verifierDetails: UpdateVerifier; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.updateOid4vpVerifier(payload.verifierDetails, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-get-oid4vp-verifier-session' }) + async getOid4vpVerifierSession(payload: { url: string; orgId: string }): Promise { + return this.agentServiceService.getOid4vpVerifierSession(payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-create-oid4vp-verification-session' }) + async oid4vpCreateVerificationSession(payload: { + sessionRequest: object; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.createOid4vpVerificationSession(payload.sessionRequest, payload.url, payload.orgId); + } } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index ec8e3c26b..fa9f71185 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -84,6 +84,7 @@ import { x509CertificateDecodeDto, X509CreateCertificateOptions } from '@credebl/common/interfaces/x509.interface'; +import { CreateVerifier, UpdateVerifier } from '@credebl/common/interfaces/oid4vp-verification'; @Injectable() @WebSocketGateway() export class AgentServiceService { @@ -2270,4 +2271,66 @@ export class AgentServiceService { throw error; } } + + async createOid4vpVerifier(verifierDetails: CreateVerifier, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const createVerifier = await this.commonService + .httpPost(url, verifierDetails, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return createVerifier; + } catch (error) { + this.logger.error(`Error in creating oid4vp verifier in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async deleteOid4vpVerifier(url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const deleteVerifier = await this.commonService.httpDelete(url, { headers: { authorization: getApiKey } }); + return deleteVerifier.data ?? deleteVerifier; + } catch (error) { + this.logger.error(`Error in deleting oid4vp verifier in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async updateOid4vpVerifier(verifierDetails: UpdateVerifier, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const updateVerifier = await this.commonService + .httpPut(url, verifierDetails, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return updateVerifier; + } catch (error) { + this.logger.error(`Error in updating oid4vp verifier in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async getOid4vpVerifierSession(url: string, orgId: string): Promise { + try { + const agentToken = await this.getOrgAgentApiKey(orgId); + const updateVerifier = await this.commonService + .httpGet(url, { headers: { authorization: agentToken } }) + .then(async (response) => response); + return updateVerifier; + } catch (error) { + this.logger.error(`Error in getting oid4vp verifier session in agent service : ${JSON.stringify(error)}`); + } + } + + async createOid4vpVerificationSession(sessionRequest: object, url: string, orgId: string): Promise { + try { + const getApiKey = await this.getOrgAgentApiKey(orgId); + const createSession = await this.commonService + .httpPost(url, sessionRequest, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return createSession; + } catch (error) { + this.logger.error(`Error in creating oid4vp verification session in agent service : ${JSON.stringify(error)}`); + throw error; + } + } } diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 370c3e381..5cc672d0d 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -32,6 +32,7 @@ import { GlobalConfigModule } from '@credebl/config/global-config.module'; import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; import { X509Module } from './x509/x509.module'; +import { Oid4vpModule } from './oid4vc-verification/oid4vc-verification.module'; @Module({ imports: [ @@ -66,6 +67,7 @@ import { X509Module } from './x509/x509.module'; GeoLocationModule, CloudWalletModule, Oid4vcIssuanceModule, + Oid4vpModule, X509Module ], controllers: [AppController], 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 new file mode 100644 index 000000000..e9a6a68cc --- /dev/null +++ b/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier-presentation.dto.ts @@ -0,0 +1,350 @@ +import { OpenId4VcVerificationPresentationState } from '@credebl/common/interfaces/oid4vp-verification'; +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional, IsString, IsUrl } from 'class-validator'; +/* eslint-disable camelcase */ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsArray, + IsBoolean, + IsDefined, + ValidateNested, + ValidatorConstraint, + ValidatorConstraintInterface, + ValidationArguments, + Validate +} from 'class-validator'; +import { Type } from 'class-transformer'; + +/** + * DTO for verification-presentation query parameters. + * Use with @Query() in your controller and enable ValidationPipe globally or on the route. + */ +export class VerificationPresentationQueryDto { + @ApiPropertyOptional({ + description: 'Public identifier of the verifier', + example: 'verifier_0x123' + }) + @IsOptional() + @IsString() + publicVerifierId?: string; + + @ApiPropertyOptional({ + description: 'Opaque payload state used by the client / verifier', + example: 'payload-state-xyz' + }) + @IsOptional() + @IsString() + payloadState?: string; + + @ApiPropertyOptional({ + description: 'Presentation state', + enum: OpenId4VcVerificationPresentationState, + example: OpenId4VcVerificationPresentationState.RequestCreated + }) + @IsOptional() + @IsEnum(OpenId4VcVerificationPresentationState) + state?: OpenId4VcVerificationPresentationState; + + @ApiPropertyOptional({ + description: 'Authorization request URI (if present)', + example: 'https://auth.example.com/request/abc123' + }) + @IsOptional() + @IsUrl() + authorizationRequestUri?: string; + + @ApiPropertyOptional({ + description: 'Nonce associated with the presentation', + example: 'n-0S6_WzA2Mj' + }) + @IsOptional() + @IsString() + nonce?: string; + + @ApiPropertyOptional({ + description: 'Optional id to target a specific presentation/resource', + example: 'presentation-id-987' + }) + @IsOptional() + @IsString() + id?: string; +} + +/** + * ----------- PEX DTOs ----------- + */ + +export class PexFieldConstraintDto { + @ApiProperty({ example: 'full_name', description: 'Field identifier' }) + @IsDefined() + @IsString() + id: string; + + @ApiProperty({ + example: ["$['full_name']"], + description: 'JSONPath location(s) of the requested claim field', + isArray: true, + type: String + }) + @IsDefined() + @IsArray() + @IsString({ each: true }) + path: string[]; + + @ApiPropertyOptional({ example: "Request holder's full name" }) + @IsOptional() + @IsString() + purpose?: string; + + @ApiPropertyOptional({ example: true }) + @IsOptional() + @IsBoolean() + intent_to_retain?: boolean; +} + +export class PexConstraintDto { + @ApiPropertyOptional({ example: 'required', description: 'Limit disclosure policy' }) + @IsOptional() + @IsString() + limit_disclosure?: string; + + @ApiProperty({ + type: [PexFieldConstraintDto], + description: 'List of requested claim fields', + example: [ + { + id: 'full_name', + path: ["$['full_name']"], + purpose: "Request holder's full name", + intent_to_retain: true + }, + { + id: 'birth_date', + path: ["$['birth_date']"], + purpose: "Request holder's birth date", + intent_to_retain: true + } + ] + }) + @IsDefined() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PexFieldConstraintDto) + fields: PexFieldConstraintDto[]; +} + +export class PexInputDescriptorDto { + @ApiProperty({ example: 'BirthCertificate-vc+sd-jwt' }) + @IsDefined() + @IsString() + id: string; + + @ApiPropertyOptional({ example: 'Birth Certificate (vc+sd-jwt)' }) + @IsOptional() + @IsString() + name?: string; + + @ApiProperty({ + example: { 'vc+sd-jwt': {} }, + description: 'Supported credential format' + }) + @IsDefined() + format: Record; + + @ApiProperty({ type: PexConstraintDto }) + @IsDefined() + @ValidateNested() + @Type(() => PexConstraintDto) + constraints: PexConstraintDto; +} + +export class PexDefinitionDto { + @ApiProperty({ example: 'BirthCertificate-verification' }) + @IsDefined() + @IsString() + id: string; + + @ApiPropertyOptional({ + example: 'Present your full name and birth date to verify the BirthCertificate offer' + }) + @IsOptional() + @IsString() + purpose?: string; + + @ApiProperty({ + type: [PexInputDescriptorDto], + description: 'Input descriptors specifying credential requirements' + }) + @IsDefined() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PexInputDescriptorDto) + input_descriptors: PexInputDescriptorDto[]; +} + +export class PresentationExchangeDto { + @ApiProperty({ type: PexDefinitionDto }) + @IsDefined() + @ValidateNested() + @Type(() => PexDefinitionDto) + definition: PexDefinitionDto; +} + +/** + * ----------- DCQL DTOs ----------- + */ + +export class DcqlClaimDto { + @ApiProperty({ + example: ['full_name'], + description: 'Claim path(s) requested from the credential' + }) + @IsDefined() + @IsArray() + @IsString({ each: true }) + path: string[]; + + @ApiPropertyOptional({ example: true }) + @IsOptional() + @IsBoolean() + intent_to_retain?: boolean; +} + +export class DcqlCredentialDto { + @ApiProperty({ example: 'birthcertificate-dc_sd_jwt' }) + @IsDefined() + @IsString() + id: string; + + @ApiProperty({ example: 'dc+sd-jwt' }) + @IsDefined() + @IsString() + format: string; + + @ApiPropertyOptional({ example: { vct: 'urn:example:vc+sd-jwt' } }) + @IsOptional() + meta?: Record; + + @ApiPropertyOptional({ example: true }) + @IsOptional() + @IsBoolean() + require_cryptographic_holder_binding?: boolean; + + @ApiProperty({ + type: [DcqlClaimDto], + description: 'List of claims requested from the credential' + }) + @IsDefined() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DcqlClaimDto) + claims: DcqlClaimDto[]; +} + +export class DcqlQueryDto { + @ApiPropertyOptional({ example: 'all' }) + @IsOptional() + @IsString() + combine?: string; + + @ApiProperty({ + type: [DcqlCredentialDto], + description: 'List of credential queries' + }) + @IsDefined() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => DcqlCredentialDto) + credentials: DcqlCredentialDto[]; +} + +export class DcqlDto { + @ApiProperty({ type: DcqlQueryDto }) + @IsDefined() + @ValidateNested() + @Type(() => DcqlQueryDto) + query: DcqlQueryDto; +} + +/** + * ----------- ROOT DTO ----------- + */ +export enum RequestSignerMethod { + DID = 'DID', + X509 = 'X509' +} + +/** + * Class-level validator: exactly one of the specified properties must be present. + */ +@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 { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object = args.object as Record; + const properties = args.constraints as string[]; + let present = 0; + for (const p of properties) { + if (object[p] !== undefined && null !== object[p]) { + present++; + } + } + return 1 === present; + } + + defaultMessage(args: ValidationArguments): string { + const properties = args.constraints as string[]; + return `Exactly one of [${properties.join(', ')}] must be provided`; + } +} + +export class PresentationRequestDto { + @ApiPropertyOptional({ + example: { + method: 'did' + } + }) + @IsOptional() + requestSigner?: { + method: RequestSignerMethod; + }; + + @ApiPropertyOptional({ + type: PresentationExchangeDto, + description: 'PEX-based presentation exchange definition' + }) + @IsOptional() + @ValidateNested() + @Type(() => PresentationExchangeDto) + presentationExchange?: PresentationExchangeDto; + + @ApiPropertyOptional({ + type: DcqlDto, + description: 'DCQL-based presentation query definition' + }) + @IsOptional() + @ValidateNested() + @Type(() => DcqlDto) + dcql?: DcqlDto; + + @ApiProperty({ + example: 'direct_post.jwt', + description: 'Response mode for the verifier' + }) + @IsDefined() + @IsString() + responseMode: string; + + /** + * 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` + * executes the validator with access to the whole object. + */ + @ApiPropertyOptional({ + description: 'Internal: ensures exactly one of dcql or presentationExchange is present' + }) + @Validate(OnlyOneOfConstraint, ['dcql', 'presentationExchange']) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _oneOfCheck?: any; +} diff --git a/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier.dto.ts b/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier.dto.ts new file mode 100644 index 000000000..17a2db754 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-verification/dtos/oid4vc-verifier.dto.ts @@ -0,0 +1,44 @@ +/* eslint-disable camelcase */ +import { IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional, OmitType, PartialType } from '@nestjs/swagger'; + +class ClientMetadataDto { + @ApiProperty({ + description: 'Name of the client application or verifier', + example: 'Example Verifier App' + }) + @IsString() + client_name: string; + + @ApiProperty({ + description: 'Logo URL of the client application', + example: 'https://example.com/logo.png' + }) + @IsString() + logo_uri: string; +} + +export class CreateVerifierDto { + @ApiProperty({ + description: 'Unique identifier for the verifier', + example: 'verifier-12345' + }) + @IsString() + verifierId: string; + + @ApiPropertyOptional({ + description: 'Optional metadata for the verifier’s client configuration', + type: () => ClientMetadataDto, + example: { + client_name: 'Example Verifier App', + logo_uri: 'https://example.com/logo.png' + } + }) + @IsOptional() + @ValidateNested() + @Type(() => ClientMetadataDto) + clientMetadata?: ClientMetadataDto; +} + +export class UpdateVerifierDto extends PartialType(OmitType(CreateVerifierDto, ['verifierId'])) {} diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts new file mode 100644 index 000000000..f748381b2 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -0,0 +1,370 @@ +/* eslint-disable default-param-last */ +/* eslint-disable no-param-reassign */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-console */ +/* eslint-disable camelcase */ +import { + Controller, + Post, + Body, + UseGuards, + HttpStatus, + Res, + Param, + UseFilters, + BadRequestException, + ParseUUIDPipe, + Get, + Query, + Put, + Delete +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiForbiddenResponse, + ApiUnauthorizedResponse, + ApiQuery +} 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 { User } from '../authz/decorators/user.decorator'; +import { ResponseMessages } from '@credebl/common/response-messages'; +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'; +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { user } from '@prisma/client'; +import { Oid4vcVerificationService } from './oid4vc-verification.service'; +import { CreateVerifierDto, UpdateVerifierDto } from './dtos/oid4vc-verifier.dto'; +import { PresentationRequestDto, VerificationPresentationQueryDto } from './dtos/oid4vc-verifier-presentation.dto'; +@Controller() +@UseFilters(CustomExceptionFilter) +@ApiTags('OID4VP') +@ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) +export class Oid4vcVerificationController { + constructor(private readonly oid4vcVerificationService: Oid4vcVerificationService) {} + /** + * Create issuer against a org(tenant) + * @param orgId The ID of the organization + * @param user The user making the request + * @param res The response object + * @returns The status of the deletion operation + */ + + @Post('/orgs/:orgId/oid4vp/verifier') + @ApiOperation({ + summary: 'Create OID4VP verifier', + description: 'Creates a new OID4VP verifier for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Verifier created successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcIssuerCreate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @User() user: user, + @Body() createVerifier: CreateVerifierDto, + @Res() res: Response + ): Promise { + const createVerifierRes = await this.oid4vcVerificationService.oid4vpCreateVerifier(createVerifier, orgId, user); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oid4vp.success.create, + data: createVerifierRes + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + /** + * Create issuer against a org(tenant) + * @param orgId The ID of the organization + * @param verifierId The ID of the Verifier + * @param user The user making the request + * @param res The response object + * @returns The status of the verifier update operation + */ + @Put('/orgs/:orgId/oid4vp/verifier/:verifierId') + @ApiOperation({ + summary: 'Update OID4VP verifier', + description: 'Updates OID4VP verifier for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Verifier updated successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async oidcIssuerUpdate( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('verifierId') + verifierId: string, + @User() user: user, + @Body() updateVerifier: UpdateVerifierDto, + @Res() res: Response + ): Promise { + console.log('This is updateVerifier', JSON.stringify(updateVerifier, null, 2)); + const createVerifierRes = await this.oid4vcVerificationService.oid4vpUpdateVerifier( + updateVerifier, + orgId, + verifierId, + user + ); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oid4vp.success.update, + data: createVerifierRes + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vp/verifier') + @ApiOperation({ + summary: 'Get OID4VP verifier details', + description: 'Retrieves details of a specific OID4VP verifier by its ID for the specified organization.' + }) + @ApiQuery({ + name: 'verifierId', + required: false, + type: String, + description: 'UUID of the verifier (optional)' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Verifier details retrieved successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getVerifierDetails( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Res() res: Response, + @Query( + 'verifierId', + new ParseUUIDPipe({ + version: '4', + optional: true, + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid verifier ID'); + } + }) + ) + verifierId?: string + ): Promise { + const verifierDetails = await this.oid4vcVerificationService.oid4vpGetVerifier(orgId, verifierId); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oid4vp.success.fetch, + data: verifierDetails + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Delete('/orgs/:orgId/oid4vp/verifier') + @ApiOperation({ + summary: 'Delete OID4VP verifier details', + description: 'Delete a specific OID4VP verifier by its ID for the specified organization.' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Verifier deleted successfully.', type: ApiResponseDto }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async deleteVerifierDetails( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Res() res: Response, + @Query( + 'verifierId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid verifier ID'); + } + }) + ) + verifierId: string + ): Promise { + const verifierDetails = await this.oid4vcVerificationService.oid4vpDeleteVerifier(orgId, verifierId); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.oid4vp.success.fetch, + data: verifierDetails + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Post('/orgs/:orgId/oid4vp/presentation') + @ApiOperation({ + summary: 'Create verification presentation', + description: 'Creates a new OID4VP verification presentation for the specified organization.' + }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Verification presentation created successfully.', + type: ApiResponseDto + }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async createVerificationPresentation( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Query( + 'verifierId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid verifier ID'); + } + }) + ) + verifierId: string, + @User() user: user, + @Body() createPresentationDto: PresentationRequestDto, + @Res() res: Response + ): Promise { + console.log('Creating verification presentation with DTO:', JSON.stringify(createPresentationDto, null, 2)); + const presentation = await this.oid4vcVerificationService.oid4vpCreateVerificationSession( + createPresentationDto, + orgId, + verifierId, + user + ); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oid4vp.success.create, + data: presentation + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Get('/orgs/:orgId/oid4vp/verifier-presentation') + @ApiOperation({ + summary: 'Get OID4VP verifier presentation details', + description: + 'Retrieves details of all OID4VP verifier presentations or a single presentation by its ID for the specified organization.' + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Verifier details retrieved successfully.', + type: ApiResponseDto + }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getVerificationPresentation( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Query() query: VerificationPresentationQueryDto, + @Res() res: Response + ): Promise { + try { + const result = await this.oid4vcVerificationService.oid4vpGetVerifierSession(orgId, query); + + return res.status(HttpStatus.OK).json({ + success: true, + message: 'Verifier details retrieved successfully.', + data: result + }); + } catch (error) { + throw new BadRequestException(error.message || 'Failed to fetch verifier presentation.'); + } + } + + @Get('/orgs/:orgId/oid4vp/verifier-presentation-response') + @ApiOperation({ + summary: 'Get OID4VP verifier presentation response details', + description: + 'Retrieves details of OID4VP verifier presentations response by its verification presentation ID for the specified organization.' + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Verifier presentation response details retrieved successfully.', + type: ApiResponseDto + }) + @ApiBearerAuth() + @Roles(OrgRoles.OWNER) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + async getVerificationPresentationResponse( + @Param( + 'orgId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Query( + 'verificationPresentationId', + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException('Invalid verificationPresentationId ID'); + } + }) + ) + verificationPresentationId: string, + @Res() res: Response + ): Promise { + try { + const result = await this.oid4vcVerificationService.getVerificationSessionResponse( + orgId, + verificationPresentationId + ); + + return res.status(HttpStatus.OK).json({ + success: true, + message: 'Verifier presentation response details retrieved successfully.', + data: result + }); + } catch (error) { + throw new BadRequestException(error.message || 'Failed to fetch verifier presentation response details.'); + } + } +} diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts new file mode 100644 index 000000000..e956de735 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts @@ -0,0 +1,24 @@ +import { Module } from '@nestjs/common'; +import { Oid4vcVerificationService } from './oid4vc-verification.service'; +import { Oid4vcVerificationController } from './oid4vc-verification.controller'; +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'; + +@Module({ + imports: [ + HttpModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.ISSUANCE_SERVICE, process.env.API_GATEWAY_NKEY_SEED) + } + ]) + ], + controllers: [Oid4vcVerificationController], + providers: [Oid4vcVerificationService, NATSClient] +}) +export class Oid4vpModule {} diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts new file mode 100644 index 000000000..b0244b87a --- /dev/null +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts @@ -0,0 +1,69 @@ +import { NATSClient } from '@credebl/common/NATSClient'; +import { Inject, Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; +// eslint-disable-next-line camelcase +import { oid4vp_verifier, user } from '@prisma/client'; +import { CreateVerifierDto, UpdateVerifierDto } from './dtos/oid4vc-verifier.dto'; +import { VerificationPresentationQueryDto } from './dtos/oid4vc-verifier-presentation.dto'; + +@Injectable() +export class Oid4vcVerificationService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly oid4vpProxy: ClientProxy, + private readonly natsClient: NATSClient + ) { + super('Oid4vcVerificationService'); + } + + async oid4vpCreateVerifier( + createVerifier: CreateVerifierDto, + orgId: string, + userDetails: user + // eslint-disable-next-line camelcase + ): Promise { + const payload = { createVerifier, orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-create', payload); + } + + async oid4vpUpdateVerifier( + updateVerifier: UpdateVerifierDto, + orgId: string, + verifierId: string, + userDetails: user + // eslint-disable-next-line camelcase + ): Promise { + const payload = { updateVerifier, orgId, verifierId, userDetails }; + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-update', payload); + } + + async oid4vpGetVerifier(orgId, verifierId?: string): Promise { + const payload = { orgId, verifierId }; + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-get', payload); + } + + async oid4vpDeleteVerifier(orgId, verifierId: string): Promise { + const payload = { orgId, verifierId }; + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-delete', payload); + } + + async oid4vpGetVerifierSession(orgId, query?: VerificationPresentationQueryDto): Promise { + const payload = { orgId, query }; + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-session-get', payload); + } + async getVerificationSessionResponse(orgId, verificationSessionId: string): Promise { + const payload = { orgId, verificationSessionId }; + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-session-response-get', payload); + } + + async oid4vpCreateVerificationSession( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sessionRequest: any, + orgId: string, + verifierId?: string, + userDetails?: user + ): Promise { + const payload = { sessionRequest, orgId, verifierId, userDetails }; + return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verification-session-create', payload); + } +} diff --git a/apps/oid4vc-verification/interfaces/oid4vp-verifier.interfaces.ts b/apps/oid4vc-verification/interfaces/oid4vp-verifier.interfaces.ts new file mode 100644 index 000000000..c39d7ae26 --- /dev/null +++ b/apps/oid4vc-verification/interfaces/oid4vp-verifier.interfaces.ts @@ -0,0 +1,27 @@ +import { OpenId4VcVerificationPresentationState } from '@credebl/common/interfaces/oid4vp-verification'; +import { organisation } from '@prisma/client'; +export interface OrgAgent { + organisation: organisation; + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + orgDid: string; + verkey: string; + agentEndPoint: string; + agentId: string; + isDidPublic: boolean; + ledgerId: string; + orgAgentTypeId: string; + tenantId: string; +} + +export interface VerificationSessionQuery { + publicVerifierId?: string; + payloadState?: string; + state?: OpenId4VcVerificationPresentationState; + authorizationRequestUri?: string; + nonce?: string; + id?: string; +} diff --git a/apps/oid4vc-verification/src/main.ts b/apps/oid4vc-verification/src/main.ts new file mode 100644 index 000000000..fbb4b7b91 --- /dev/null +++ b/apps/oid4vc-verification/src/main.ts @@ -0,0 +1,23 @@ +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 { Oid4vpModule } from './oid4vc-verification.module'; + +const logger = new Logger(); + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice(Oid4vpModule, { + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.OIDC4VC_VERIFICATION_SERVICE, process.env.Verification_NKEY_SEED) + }); + app.useLogger(app.get(NestjsLoggerServiceAdapter)); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + logger.log('OID4VC-Verification-Service Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts new file mode 100644 index 000000000..52a010756 --- /dev/null +++ b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts @@ -0,0 +1,73 @@ +import { Controller } from '@nestjs/common'; +import { Oid4vpVerificationService } from './oid4vc-verification.service'; +import { user } from '@prisma/client'; +import { CreateVerifier, UpdateVerifier } from '@credebl/common/interfaces/oid4vp-verification'; +import { MessagePattern } from '@nestjs/microservices'; +import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfaces'; + +@Controller() +export class Oid4vpVerificationController { + constructor(private readonly oid4vpVerificationService: Oid4vpVerificationService) { } + + @MessagePattern({ cmd: 'oid4vp-verifier-create' }) + async oid4vpCreateVerifier(payload: { + createVerifier: CreateVerifier; + orgId: string; + userDetails: user; + }): Promise { + const { createVerifier, orgId, userDetails } = payload; + return this.oid4vpVerificationService.oid4vpCreateVerifier(createVerifier, orgId, userDetails); + } + + @MessagePattern({ cmd: 'oid4vp-verifier-update' }) + async oid4vpUpdateVerifier(payload: { + updateVerifier: UpdateVerifier; + orgId: string; + verifierId: string; + userDetails: user; + }): Promise { + const { updateVerifier, orgId, verifierId, userDetails } = payload; + return this.oid4vpVerificationService.oid4vpUpdateVerifier(updateVerifier, orgId, verifierId, userDetails); + } + + @MessagePattern({ cmd: 'oid4vp-verifier-get' }) + async oid4vpGetVerifier(payload: { orgId: string; verifierId?: string }): Promise { + const { orgId, verifierId } = payload; + return this.oid4vpVerificationService.getVerifierById(orgId, verifierId); + } + + @MessagePattern({ cmd: 'oid4vp-verifier-delete' }) + async oid4vpDeleteVerifier(payload: { orgId: string; verifierId: string }): Promise { + const { orgId, verifierId } = payload; + return this.oid4vpVerificationService.deleteVerifierById(orgId, verifierId); + } + + @MessagePattern({ cmd: 'oid4vp-verifier-session-get' }) + async oid4vpGetVerifierSession(payload: { orgId: string; query?: VerificationSessionQuery }): Promise { + const { orgId, query } = payload; + return this.oid4vpVerificationService.getVerifierSession(orgId, query); + } + + @MessagePattern({ cmd: 'oid4vp-verifier-session-response-get' }) + async getVerificationSessionResponse(payload: { orgId: string; verificationSessionId: string }): Promise { + const { orgId, verificationSessionId } = payload; + return this.oid4vpVerificationService.getVerificationSessionResponse(orgId, verificationSessionId); + } + + @MessagePattern({ cmd: 'oid4vp-verification-session-create' }) + async oid4vpCreateVerificationSession(payload: { + orgId: string; + verifierId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sessionRequest: any; + userDetails: user; + }): Promise { + const { orgId, verifierId, sessionRequest, userDetails } = payload; + return this.oid4vpVerificationService.oid4vpCreateVerificationSession( + orgId, + verifierId, + sessionRequest, + userDetails + ); + } +} diff --git a/apps/oid4vc-verification/src/oid4vc-verification.module.ts b/apps/oid4vc-verification/src/oid4vc-verification.module.ts new file mode 100644 index 000000000..78b37ac7e --- /dev/null +++ b/apps/oid4vc-verification/src/oid4vc-verification.module.ts @@ -0,0 +1,39 @@ +import { Logger, Module } from '@nestjs/common'; +import { Oid4vpVerificationController } from './oid4vc-verification.controller'; +import { Oid4vpVerificationService } from './oid4vc-verification.service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonModule } from '@credebl/common'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { GlobalConfigModule } from '@credebl/config'; +import { ContextInterceptorModule } from '@credebl/context'; +import { LoggerModule } from '@credebl/logger'; +import { CacheModule } from '@nestjs/cache-manager'; +import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; +import { NATSClient } from '@credebl/common/NATSClient'; +import { PrismaService } from '@credebl/prisma-service'; +import { Oid4vpRepository } from './oid4vc-verification.repository'; + +@Module({ + imports: [ + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions( + CommonConstants.OIDC4VC_VERIFICATION_SERVICE, + process.env.OIDC4VC_VERIFICATION_NKEY_SEED + ) + } + ]), + CommonModule, + GlobalConfigModule, + LoggerModule, + PlatformConfig, + ContextInterceptorModule, + CacheModule.register() + ], + controllers: [Oid4vpVerificationController], + providers: [Oid4vpVerificationService, Oid4vpRepository, PrismaService, Logger, NATSClient] +}) +export class Oid4vpModule {} diff --git a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts new file mode 100644 index 000000000..9d409237b --- /dev/null +++ b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts @@ -0,0 +1,163 @@ +/* eslint-disable camelcase */ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +// eslint-disable-next-line camelcase +import { oid4vp_verifier, org_agents } from '@prisma/client'; +import { PrismaService } from '@credebl/prisma-service'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { OrgAgent } from '../interfaces/oid4vp-verifier.interfaces'; + +@Injectable() +export class Oid4vpRepository { + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} + + async getAgentEndPoint(orgId: string): Promise { + try { + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { + orgId + }, + include: { + organisation: true + } + }); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + return agentDetails; + } catch (error) { + this.logger.error(`Error in get getAgentEndPoint: ${error.message} `); + throw error; + } + } + + async getOrgAgentType(orgAgentId: string): Promise { + try { + const { agent } = await this.prisma.org_agents_type.findFirst({ + where: { + id: orgAgentId + } + }); + + return agent; + } catch (error) { + this.logger.error(`[getOrgAgentType] - error: ${JSON.stringify(error)}`); + throw error; + } + } + + async getOrganizationByTenantId(tenantId: string): Promise { + try { + return this.prisma.org_agents.findFirst({ + where: { + tenantId + } + }); + } catch (error) { + this.logger.error(`Error in getOrganization in issuance repository: ${error.message} `); + throw error; + } + } + + async createOid4vpVerifier(verifierDetails, orgId: string, userId: string): Promise { + try { + const { id, clientMetadata, verifierId } = verifierDetails; + return this.prisma.oid4vp_verifier.create({ + data: { + metadata: clientMetadata, + publicVerifierId: verifierId, + verifierId: id, + createdBy: userId, + lastChangedBy: userId, + orgAgentId: orgId + } + }); + } catch (error) { + this.logger.error(`Error in createOid4vpVerifier: ${error.message}`); + throw error; + } + } + + async updateOid4vpVerifier(verifierDetails, userId: string, verifierId: string): Promise { + try { + const { clientMetadata } = verifierDetails; + return this.prisma.oid4vp_verifier.update({ + data: { + metadata: clientMetadata, + lastChangedBy: userId + }, + where: { + id: verifierId + } + }); + } catch (error) { + this.logger.error(`Error in createOid4vpVerifier: ${error.message}`); + throw error; + } + } + + async getVerifiersByPublicVerifierId(publicVerifierId: string): Promise { + try { + return await this.prisma.oid4vp_verifier.findMany({ + where: { + publicVerifierId + } + }); + } catch (error) { + this.logger.error(`Error in getVerifiersByPublicVerifierId: ${error.message}`); + throw error; + } + } + + async getVerifiersByVerifierId(orgId: string, verifierId?: string): Promise { + try { + return await this.prisma.oid4vp_verifier.findMany({ + where: { + id: verifierId, + orgAgent: { + orgId + } + } + }); + } catch (error) { + this.logger.error(`Error in getVerifiersByPublicVerifierId: ${error.message}`); + throw error; + } + } + + async getVerifierById(orgId: string, verifierId?: string): Promise { + try { + return await this.prisma.oid4vp_verifier.findUnique({ + where: { + id: verifierId, + orgAgent: { + orgId + } + } + }); + } catch (error) { + this.logger.error(`Error in getVerifiersByPublicVerifierId: ${error.message}`); + throw error; + } + } + + async deleteVerifierByVerifierId(orgId: string, verifierId?: string): Promise { + try { + return await this.prisma.oid4vp_verifier.delete({ + where: { + id: verifierId, + orgAgent: { + orgId + } + } + }); + } catch (error) { + this.logger.error(`Error in deleteVerifierByVerifierId: ${error.message}`); + throw error; + } + } +} diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts new file mode 100644 index 000000000..fb367741b --- /dev/null +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -0,0 +1,371 @@ +/* eslint-disable quotes */ +/* eslint-disable no-useless-catch */ +/* eslint-disable camelcase */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ + +import { + ConflictException, + HttpException, + Inject, + Injectable, + InternalServerErrorException, + Logger, + NotFoundException +} from '@nestjs/common'; +import { Oid4vpRepository } from './oid4vc-verification.repository'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { getAgentUrl } from '@credebl/common/common.utils'; +import { user } from '@prisma/client'; +import { map } from 'rxjs'; +import { CreateVerifier, UpdateVerifier, VerifierRecord } from '@credebl/common/interfaces/oid4vp-verification'; +import { buildUrlWithQuery } from '@credebl/common/cast.helper'; +import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfaces'; + +@Injectable() +export class Oid4vpVerificationService { + private readonly logger = new Logger('IssueCredentialService'); + constructor( + @Inject('NATS_CLIENT') private readonly oid4vpVerificationServiceProxy: ClientProxy, + private readonly oid4vpRepository: Oid4vpRepository + ) {} + + async oid4vpCreateVerifier(createVerifier: CreateVerifier, orgId: string, userDetails: user): Promise { + try { + let createdVerifierDetails; + const { verifierId } = createVerifier; + const checkIdExist = await this.oid4vpRepository.getVerifiersByPublicVerifierId(verifierId); + if (0 < checkIdExist.length) { + throw new ConflictException(ResponseMessages.oid4vp.error.verifierIdAlreadyExists); + } + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id } = agentDetails; + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_CREATE); + console.log('url:::', url); + try { + createdVerifierDetails = await this._createOid4vpVerifier(createVerifier, url, orgId); + if (!createdVerifierDetails.response) { + throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); + } + createdVerifierDetails = createdVerifierDetails.response as VerifierRecord; + console.log('createdVerifierDetails', createdVerifierDetails); + } catch (error) { + const status409 = + 409 === error?.status?.message?.statusCode || 409 === error?.response?.status || 409 === error?.statusCode; + + if (status409) { + throw new ConflictException(`Verifier with id '${createdVerifierDetails.verifierId}' already exists`); + } + throw error; + } + const saveVerifierDetails = await this.oid4vpRepository.createOid4vpVerifier( + createdVerifierDetails, + id, + userDetails.id + ); + console.log('saveVerifierDetails', saveVerifierDetails); + if (!saveVerifierDetails) { + throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); + } + return saveVerifierDetails; + } catch (error) { + this.logger.error( + `[oid4vpCreateVerifier] - error in oid4vpCreateVerifier issuance records: ${JSON.stringify(error)}` + ); + throw new RpcException(error?.response ?? error); + } + } + + async oid4vpUpdateVerifier( + updateVerifier: UpdateVerifier, + orgId: string, + verifierId: string, + userDetails: user + ): Promise { + try { + let updatedVerifierDetails; + const existingVerifiers = await this.oid4vpRepository.getVerifiersByVerifierId(verifierId); + if (0 > existingVerifiers.length) { + throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + } + // updateVerifier['verifierId'] = existingVerifiers[0].publicVerifierId + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id } = agentDetails; + const url = await getAgentUrl( + agentEndPoint, + CommonConstants.OIDC_VERIFIER_UPDATE, + existingVerifiers[0].publicVerifierId + ); + console.log('url:::', url); + try { + updatedVerifierDetails = await this._updateOid4vpVerifier(updateVerifier, url, orgId); + if (!updatedVerifierDetails.response) { + throw new InternalServerErrorException(ResponseMessages.oid4vp.error.updateFailed); + } + updatedVerifierDetails = updatedVerifierDetails.response.data as VerifierRecord; + } catch (error) { + // We'll not need this + const status409 = + 409 === error?.status?.message?.statusCode || 409 === error?.response?.status || 409 === error?.statusCode; + + if (status409) { + throw new ConflictException(`Verifier with id '${updatedVerifierDetails.verifierId}' already exists`); + } + throw error; + } + const updateVerifierDetails = await this.oid4vpRepository.updateOid4vpVerifier( + updatedVerifierDetails, + userDetails.id, + verifierId + ); + if (!updateVerifierDetails) { + throw new InternalServerErrorException(ResponseMessages.oid4vp.error.updateFailed); + } + return updateVerifierDetails; + } catch (error) { + this.logger.error(`[oid4vpUpdateVerifier] - error in oid4vpUpdateVerifier records: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async getVerifierById(orgId: string, verifierId?: string): Promise { + try { + const verifiers = await this.oid4vpRepository.getVerifiersByVerifierId(orgId, verifierId); + if (!verifiers || 0 === verifiers.length) { + throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + } + return verifiers; + } catch (error) { + this.logger.error(`[getVerifierById] - error: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async deleteVerifierById(orgId: string, verifierId: string): Promise { + try { + const checkIdExist = await this.oid4vpRepository.getVerifiersByVerifierId(orgId, verifierId); + if (0 == checkIdExist.length) { + throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + } + + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id } = agentDetails; + const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_DELETE, checkIdExist[0].verifierId); + console.log('url:::', url); + + await this._deleteOid4vpVerifier(url, orgId); + + const verifier = await this.oid4vpRepository.deleteVerifierByVerifierId(orgId, verifierId); + return verifier; + } catch (error) { + this.logger.error(`[deleteVerifierById] - error: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error.error ?? error); + } + } + + async oid4vpCreateVerificationSession(orgId, verifierId, sessionRequest, userDetails: user): Promise { + try { + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, orgDid } = agentDetails; + + const getVerifierDetails = await this.oid4vpRepository.getVerifierById(orgId, verifierId); + + if (!getVerifierDetails) { + throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + } + sessionRequest.verifierId = getVerifierDetails.publicVerifierId; + if ('did' === sessionRequest.requestSigner.method) { + sessionRequest.requestSigner.didUrl = orgDid; + } + const url = await getAgentUrl(agentEndPoint, CommonConstants.OID4VP_VERIFICATION_SESSION); + this.logger.log(`[oid4vpCreateVerificationSession] calling agent url: ${url}`); + // console.log('Final sessionRequest', JSON.stringify(sessionRequest, null, 2)); + const createdSession = await this._createVerificationSession(sessionRequest, url, orgId); + if (!createdSession?.response) { + throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); + } + + return createdSession.response; + } catch (error) { + this.logger.error( + `[oid4vpCreateVerificationSession] - error creating verification session: ${JSON.stringify(error)}` + ); + throw new RpcException(error?.response ?? error); + } + } + + async getVerifierSession(orgId: string, query?: VerificationSessionQuery): Promise { + try { + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id } = agentDetails; + + let url = query.id + ? getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_SESSION_GET_BY_ID, query.id) + : getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_SESSION_GET_BY_QUERY); + + if (!query.id) { + url = buildUrlWithQuery(url, query); + } + console.log('url:::', url); + + const verifiers = await await this._getOid4vpVerifierSession(url, orgId); + if (!verifiers || 0 === verifiers.length) { + throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + } + return verifiers; + } catch (error) { + this.logger.error(`[getVerifierSession] - error: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async getVerificationSessionResponse(orgId: string, verificationSessionId: string): Promise { + try { + const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + const { agentEndPoint, id } = agentDetails; + + const url = getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_SESSION_GET_BY_ID, verificationSessionId); + console.log('url:::', url); + + const verifiers = await await this._getOid4vpVerifierSession(url, orgId); + if (!verifiers || 0 === verifiers.length) { + throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + } + return verifiers; + } catch (error) { + this.logger.error(`[getVerificationSessionResponse] - error: ${JSON.stringify(error)}`); + throw new RpcException(error?.response ?? error); + } + } + + async _createOid4vpVerifier(verifierDetails: CreateVerifier, url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'agent-create-oid4vp-verifier' }; + const payload = { verifierDetails, url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_createOID4VPVerifier] [NATS call]- error in create OID4VP Verifier : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _deleteOid4vpVerifier(url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'agent-delete-oid4vp-verifier' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_deleteOid4vpVerifier] [NATS call]- error in delete OID4VP Verifier : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _updateOid4vpVerifier(verifierDetails: UpdateVerifier, url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'agent-update-oid4vp-verifier' }; + const payload = { verifierDetails, url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_updateOid4vpVerifier] [NATS call]- error in update OID4VP Verifier : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _createVerificationSession(sessionRequest: any, url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'agent-create-oid4vp-verification-session' }; + const payload = { sessionRequest, url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_createVerificationSession] [NATS call]- error in create verification session : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _getOid4vpVerifierSession(url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'agent-get-oid4vp-verifier-session' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_getOid4vpVerifierSession] [NATS call]- error in get OID4VP Verifier Session : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async _getVerificationSessionResponse(url: string, orgId: string): Promise { + try { + const pattern = { cmd: 'agent-get-oid4vp-verifier-session' }; + const payload = { url, orgId }; + return this.natsCall(pattern, payload); + } catch (error) { + this.logger.error( + `[_getVerificationSessionResponse] [NATS call]- error in get OID4VP Verifier Session : ${JSON.stringify(error)}` + ); + throw error; + } + } + + async natsCall( + pattern: object, + payload: object + ): Promise<{ + response: string; + }> { + try { + return this.oid4vpVerificationServiceProxy + .send(pattern, payload) + .pipe( + map((response) => ({ + response + })) + ) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.statusCode, + error: error.message + }, + error.error + ); + }); + } catch (error) { + this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); + throw error; + } + } +} diff --git a/apps/oid4vc-verification/test/app.e2e-spec.ts b/apps/oid4vc-verification/test/app.e2e-spec.ts new file mode 100644 index 000000000..4bb294287 --- /dev/null +++ b/apps/oid4vc-verification/test/app.e2e-spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { Oid4vcVerificationModule } from './../src/oid4vc-verification.module'; + +describe('Oid4vcVerificationController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [Oid4vcVerificationModule] + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => request(app.getHttpServer()).get('/').expect(200).expect('Hello World!')); +}); diff --git a/apps/oid4vc-verification/test/jest-e2e.json b/apps/oid4vc-verification/test/jest-e2e.json new file mode 100644 index 000000000..e9d912f3e --- /dev/null +++ b/apps/oid4vc-verification/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/oid4vc-verification/tsconfig.app.json b/apps/oid4vc-verification/tsconfig.app.json new file mode 100644 index 000000000..2a2de5a9b --- /dev/null +++ b/apps/oid4vc-verification/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/oid4vc-verification" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/common/src/cast.helper.ts b/libs/common/src/cast.helper.ts index 7d030a6c9..a4da5a2bb 100644 --- a/libs/common/src/cast.helper.ts +++ b/libs/common/src/cast.helper.ts @@ -523,3 +523,20 @@ export function ValidateNestedStructureFields(validationOptions?: ValidationOpti }); }; } + +export function buildUrlWithQuery>(baseUrl: string, queryParams: T): string { + const criteriaParams: string[] = []; + + if (!queryParams || (queryParams?.length >= 0)) { + return baseUrl + } + + for (const [key, value] of Object.entries(queryParams)) { + // Skip undefined or null values + if (value !== undefined && value !== null) { + criteriaParams.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + } + } + + return criteriaParams.length > 0 ? `${baseUrl}?${criteriaParams.join('&')}` : baseUrl; +} \ No newline at end of file diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 4254ad114..201a1817b 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -126,6 +126,16 @@ export enum CommonConstants { URL_OIDC_ISSUER_SESSIONS_GET = '/openid4vc/issuance-sessions/#', URL_OIDC_ISSUER_SESSIONS_GET_ALL = '/openid4vc/issuance-sessions', + // OID4Vp URLs + URL_OIDC_VERIFIER_CREATE = '/openid4vc/verifier', //TODO: correct this URL + URL_OIDC_VERIFIER_UPDATE = '/openid4vc/verifier/#', + URL_OIDC_VERIFIER_DELETE = '/openid4vc/verifier/#', + URL_OIDC_VERIFIER_GET = '/openid4vc/verifier/#', + URL_OIDC_VERIFIER_SESSION_GET_BY_ID = '/openid4vc/verification-sessions/#', + URL_OIDC_VERIFIER_SESSION_GET_BY_QUERY = '/openid4vc/verification-sessions', + URL_OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID = '/openid4vc/verification-sessions/response#', + URL_OID4VP_VERIFICATION_SESSION = '/openid4vc/verification-sessions/create-presentation-request', + //X509 agent API URLs URL_CREATE_X509_CERTIFICATE = '/x509', URL_IMPORT_X509_CERTIFICATE = '/x509/import', @@ -379,6 +389,8 @@ export enum CommonConstants { GEO_LOCATION_SERVICE = 'geo-location', CLOUD_WALLET_SERVICE = 'cloud-wallet', OIDC4VC_ISSUANCE_SERVICE = 'oid4vc-issuance', + OIDC4VC_VERIFICATION_SERVICE = 'oid4vc-verification', + OID4VP_VERIFICATION_SESSION = 'oid4vp-verification-session', X509_SERVICE = 'x509-service', ACCEPT_OFFER = '/didcomm/credentials/accept-offer', @@ -413,6 +425,14 @@ export enum CommonConstants { OIDC_ISSUER_SESSIONS = 'get-oid4vc-sessions', OIDC_DELETE_CREDENTIAL_OFFER = 'delete-oid4vc-credential-offer', + // OID4VP + OIDC_VERIFIER_CREATE = 'create-oid4vp-verifier', + OIDC_VERIFIER_UPDATE = 'update-oid4vp-verifier', + OIDC_VERIFIER_DELETE = 'delete-oid4vp-verifier', + OIDC_VERIFIER_SESSION_GET_BY_ID = 'get-oid4vp-verifier-session-id', + OIDC_VERIFIER_SESSION_GET_BY_QUERY = 'get-oid4vp-verifier-session-query', + OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID = 'get-oid4vp-verifier-session-response-id', + //X509 X509_CREATE_CERTIFICATE = 'create-x509-certificate', X509_IMPORT_CERTIFICATE = 'import-x509-certificate', diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index a41b9b72d..6ed8bdeae 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -67,7 +67,7 @@ export const networkNamespace = (did: string): string => { return segments[1]; }; -export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramId?: string): Promise => { +export const getAgentUrl = (agentEndPoint: string, urlFlag: string, paramId?: string): string => { if (!agentEndPoint) { throw new NotFoundException(ResponseMessages.common.error.invalidEndpoint); } @@ -108,7 +108,23 @@ export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramI [String(CommonConstants.OIDC_DELETE_CREDENTIAL_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], [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)] + [String(CommonConstants.X509_IMPORT_CERTIFICATE), String(CommonConstants.URL_IMPORT_X509_CERTIFICATE)], + [String(CommonConstants.OIDC_VERIFIER_CREATE), String(CommonConstants.URL_OIDC_VERIFIER_CREATE)], + [String(CommonConstants.OIDC_VERIFIER_UPDATE), String(CommonConstants.URL_OIDC_VERIFIER_UPDATE)], + [String(CommonConstants.OIDC_VERIFIER_DELETE), String(CommonConstants.URL_OIDC_VERIFIER_DELETE)], + [ + String(CommonConstants.OIDC_VERIFIER_SESSION_GET_BY_ID), + String(CommonConstants.URL_OIDC_VERIFIER_SESSION_GET_BY_ID) + ], + [ + String(CommonConstants.OIDC_VERIFIER_SESSION_GET_BY_QUERY), + String(CommonConstants.URL_OIDC_VERIFIER_SESSION_GET_BY_QUERY) + ], + [ + String(CommonConstants.OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID), + String(CommonConstants.URL_OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID) + ], + [String(CommonConstants.OID4VP_VERIFICATION_SESSION), String(CommonConstants.URL_OID4VP_VERIFICATION_SESSION)] ]); const urlSuffix = agentUrlMap.get(urlFlag); diff --git a/libs/common/src/interfaces/oid4vp-verification.ts b/libs/common/src/interfaces/oid4vp-verification.ts new file mode 100644 index 000000000..8c0e8b447 --- /dev/null +++ b/libs/common/src/interfaces/oid4vp-verification.ts @@ -0,0 +1,33 @@ +export interface ClientMetadata { + client_name: string; + logo_uri: string; +} + +export interface CreateVerifier { + verifierId: string; + clientMetadata?: ClientMetadata; +} + +export interface UpdateVerifier extends Omit { + publicVerifierId?: string; +} + +export interface VerifierRecord { + _tags: Record; + metadata: Record; + id: string; + createdAt: string; // ISO timestamp + verifierId: string; + clientMetadata: { + client_name: string; + logo_uri: string; + }; + updatedAt: string; // ISO timestamp +} + +export enum OpenId4VcVerificationPresentationState { + RequestCreated = 'RequestCreated', + RequestUriRetrieved = 'RequestUriRetrieved', + ResponseVerified = 'ResponseVerified', + Error = 'Error' +} diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index e4a5647e9..6a56850ed 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -557,6 +557,26 @@ export const ResponseMessages = { missingValidityInfo: 'Validity Info(validFrom, validTo) is required for validity of credential' } }, + oid4vp: { + success: { + create: 'OID4VP verifier created successfully.', + update: 'OID4VP verifier updated successfully.', + delete: 'OID4VP verifier deleted successfully.', + fetch: 'OID4VP verifier(s) fetched successfully.', + getById: 'OID4VP verifier details fetched successfully.' + }, + error: { + notFound: 'OID4VP verifier not found.', + invalidId: 'Invalid OID4VP verifier ID.', + createFailed: 'Failed to create OID4VP verifier.', + updateFailed: 'Failed to update OID4VP verifier.', + deleteFailed: 'Failed to delete OID4VP verifier.', + notFoundIssuerDisplay: 'Issuer display not found.', + notFoundIssuerDetails: 'Issuer details not found.', + verifierIdAlreadyExists: 'Verifier ID already exists for this verifier.', + deleteTemplate: 'Error while deleting template.' + } + }, x509: { success: { create: 'x509 certificate created successfully', diff --git a/libs/prisma-service/prisma/migrations/20251016105943_added_new_oid4vp_verifier_table/migration.sql b/libs/prisma-service/prisma/migrations/20251016105943_added_new_oid4vp_verifier_table/migration.sql new file mode 100644 index 000000000..4a31a1b31 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251016105943_added_new_oid4vp_verifier_table/migration.sql @@ -0,0 +1,24 @@ +-- CreateTable +CREATE TABLE "oid4vp_verifier" ( + "id" UUID NOT NULL, + "publicVerifierId" TEXT NOT NULL, + "metadata" JSONB NOT NULL, + "verifierId" TEXT NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" TEXT NOT NULL DEFAULT '1', + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" TEXT NOT NULL DEFAULT '1', + "deletedAt" TIMESTAMP(6), + "orgAgentId" UUID NOT NULL, + + CONSTRAINT "oid4vp_verifier_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "oid4vp_verifier_verifierId_key" ON "oid4vp_verifier"("verifierId"); + +-- CreateIndex +CREATE INDEX "oid4vp_verifier_orgAgentId_idx" ON "oid4vp_verifier"("orgAgentId"); + +-- AddForeignKey +ALTER TABLE "oid4vp_verifier" ADD CONSTRAINT "oid4vp_verifier_orgAgentId_fkey" FOREIGN KEY ("orgAgentId") REFERENCES "org_agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/migrations/20251103071640_updated_table_oid4vp_verifier_to_remove_unnecessary_column_deleted_at/migration.sql b/libs/prisma-service/prisma/migrations/20251103071640_updated_table_oid4vp_verifier_to_remove_unnecessary_column_deleted_at/migration.sql new file mode 100644 index 000000000..3008e2110 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251103071640_updated_table_oid4vp_verifier_to_remove_unnecessary_column_deleted_at/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `deletedAt` on the `oid4vp_verifier` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "oid4vp_verifier" DROP COLUMN "deletedAt"; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 5d3408b53..1a95f1e6a 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -232,6 +232,7 @@ model org_agents { org_dids org_dids[] oidc_issuer oidc_issuer[] x509_certificates x509_certificates[] + oid4vp_verifiers oid4vp_verifier[] } model org_dids { @@ -648,3 +649,17 @@ model x509_certificates { lastChangedDateTime DateTime @updatedAt lastChangedBy String @db.Uuid } + +model oid4vp_verifier { + id String @id @default(uuid()) @db.Uuid + publicVerifierId String + metadata Json + verifierId String @unique + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @default("1") + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @default("1") + orgAgentId String @db.Uuid + orgAgent org_agents @relation(fields: [orgAgentId], references: [id]) + @@index([orgAgentId]) +} diff --git a/nest-cli.json b/nest-cli.json index 82ff28c5d..663799cc6 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -8,40 +8,40 @@ "tsConfigPath": "apps/api-gateway/tsconfig.app.json" }, "projects": { - "api-gateway": { + "agent-provisioning": { "type": "application", - "root": "apps/api-gateway", + "root": "apps/agent-provisioning", "entryFile": "main", - "sourceRoot": "apps/api-gateway/src", + "sourceRoot": "apps/agent-provisioning/src", "compilerOptions": { - "tsConfigPath": "apps/api-gateway/tsconfig.app.json" + "tsConfigPath": "apps/agent-provisioning/tsconfig.app.json" } }, - "platform-service": { + "agent-service": { "type": "application", - "root": "apps/platform-service", + "root": "apps/agent-service", "entryFile": "main", - "sourceRoot": "apps/platform-service/src", + "sourceRoot": "apps/agent-service/src", "compilerOptions": { - "tsConfigPath": "apps/platform-service/tsconfig.app.json" + "tsConfigPath": "apps/agent-service/tsconfig.app.json" } }, - "common": { - "type": "library", - "root": "libs/common", - "entryFile": "index", - "sourceRoot": "libs/common/src", + "api-gateway": { + "type": "application", + "root": "apps/api-gateway", + "entryFile": "main", + "sourceRoot": "apps/api-gateway/src", "compilerOptions": { - "tsConfigPath": "libs/common/tsconfig.lib.json" + "tsConfigPath": "apps/api-gateway/tsconfig.app.json" } }, - "keycloak-url": { + "aws": { "type": "library", - "root": "libs/keycloak-url", + "root": "libs/aws", "entryFile": "index", - "sourceRoot": "libs/keycloak-url/src", + "sourceRoot": "libs/aws/src", "compilerOptions": { - "tsConfigPath": "libs/keycloak-url/tsconfig.lib.json" + "tsConfigPath": "libs/aws/tsconfig.lib.json" } }, "client-registration": { @@ -53,40 +53,49 @@ "tsConfigPath": "libs/client-registration/tsconfig.lib.json" } }, - "connection": { + "cloud-wallet": { "type": "application", - "root": "apps/connection", + "root": "apps/cloud-wallet", "entryFile": "main", - "sourceRoot": "apps/connection/src", + "sourceRoot": "apps/cloud-wallet/src", "compilerOptions": { - "tsConfigPath": "apps/connection/tsconfig.app.json" + "tsConfigPath": "apps/cloud-wallet/tsconfig.app.json" } }, - "prisma": { + "common": { "type": "library", - "root": "libs/prisma", + "root": "libs/common", "entryFile": "index", - "sourceRoot": "libs/prisma/src", + "sourceRoot": "libs/common/src", "compilerOptions": { - "tsConfigPath": "libs/prisma/tsconfig.lib.json" + "tsConfigPath": "libs/common/tsconfig.lib.json" } }, - "repositories": { + "config": { "type": "library", - "root": "libs/repositories", + "root": "libs/config", "entryFile": "index", - "sourceRoot": "libs/repositories/src", + "sourceRoot": "libs/config/src", "compilerOptions": { - "tsConfigPath": "libs/repositories/tsconfig.lib.json" + "tsConfigPath": "libs/config/tsconfig.lib.json" } }, - "user-request": { + "connection": { + "type": "application", + "root": "apps/connection", + "entryFile": "main", + "sourceRoot": "apps/connection/src", + "compilerOptions": { + "tsConfigPath": "apps/connection/tsconfig.app.json" + } + }, + "context": { "type": "library", - "root": "libs/user-request", + "root": "libs/context", "entryFile": "index", - "sourceRoot": "libs/user-request/src", + "sourceRoot": "libs/context/src", "compilerOptions": { - "tsConfigPath": "libs/user-request/tsconfig.lib.json" + "tsConfigPath": "libs/context/tsconfig.lib.json" } }, "enum": { @@ -98,49 +107,31 @@ "tsConfigPath": "libs/enum/tsconfig.lib.json" } }, - "prisma-service": { - "type": "library", - "root": "libs/prisma-service", - "entryFile": "index", - "sourceRoot": "libs/prisma-service/src", - "compilerOptions": { - "tsConfigPath": "libs/prisma-service/tsconfig.lib.json" - } - }, - "organization": { + "geo-location": { "type": "application", - "root": "apps/organization", + "root": "apps/geo-location", "entryFile": "main", - "sourceRoot": "apps/organization/src", + "sourceRoot": "apps/geo-location/src", "compilerOptions": { - "tsConfigPath": "apps/organization/tsconfig.app.json" + "tsConfigPath": "apps/geo-location/tsconfig.app.json" } }, - "user": { + "issuance": { "type": "application", - "root": "apps/user", + "root": "apps/issuance", "entryFile": "main", - "sourceRoot": "apps/user/src", - "compilerOptions": { - "tsConfigPath": "apps/user/tsconfig.app.json" - } - }, - "org-roles": { - "type": "library", - "root": "libs/org-roles", - "entryFile": "index", - "sourceRoot": "libs/org-roles/src", + "sourceRoot": "apps/issuance/src", "compilerOptions": { - "tsConfigPath": "libs/org-roles/tsconfig.lib.json" + "tsConfigPath": "apps/issuance/tsconfig.app.json" } }, - "user-org-roles": { + "keycloak-url": { "type": "library", - "root": "libs/user-org-roles", + "root": "libs/keycloak-url", "entryFile": "index", - "sourceRoot": "libs/user-org-roles/src", + "sourceRoot": "libs/keycloak-url/src", "compilerOptions": { - "tsConfigPath": "libs/user-org-roles/tsconfig.lib.json" + "tsConfigPath": "libs/keycloak-url/tsconfig.lib.json" } }, "ledger": { @@ -152,151 +143,169 @@ "tsConfigPath": "apps/ledger/tsconfig.app.json" } }, - "agent-service": { - "type": "application", - "root": "apps/agent-service", - "entryFile": "main", - "sourceRoot": "apps/agent-service/src", + "logger": { + "type": "library", + "root": "libs/logger", + "entryFile": "index", + "sourceRoot": "libs/logger/src", "compilerOptions": { - "tsConfigPath": "apps/agent-service/tsconfig.app.json" + "tsConfigPath": "libs/logger/tsconfig.lib.json" } }, - "agent-provisioning": { + "notification": { "type": "application", - "root": "apps/agent-provisioning", + "root": "apps/notification", "entryFile": "main", - "sourceRoot": "apps/agent-provisioning/src", + "sourceRoot": "apps/notification/src", "compilerOptions": { - "tsConfigPath": "apps/agent-provisioning/tsconfig.app.json" + "tsConfigPath": "apps/notification/tsconfig.app.json" } }, - "issuance": { + "oid4vc-issuance": { "type": "application", - "root": "apps/issuance", + "root": "apps/oid4vc-issuance", "entryFile": "main", - "sourceRoot": "apps/issuance/src", + "sourceRoot": "apps/oid4vc-issuance/src", "compilerOptions": { - "tsConfigPath": "apps/issuance/tsconfig.app.json" + "tsConfigPath": "apps/oid4vc-issuance/tsconfig.app.json" } }, - "verification": { + "oid4vc-verification": { "type": "application", - "root": "apps/verification", + "root": "apps/oid4vc-verification", "entryFile": "main", - "sourceRoot": "apps/verification/src", + "sourceRoot": "apps/oid4vc-verification/src", "compilerOptions": { - "tsConfigPath": "apps/verification/tsconfig.app.json" + "tsConfigPath": "apps/oid4vc-verification/tsconfig.app.json" } }, - "user-activity": { + "org-roles": { "type": "library", - "root": "libs/user-activity", + "root": "libs/org-roles", "entryFile": "index", - "sourceRoot": "libs/user-activity/src", + "sourceRoot": "libs/org-roles/src", "compilerOptions": { - "tsConfigPath": "libs/user-activity/tsconfig.lib.json" + "tsConfigPath": "libs/org-roles/tsconfig.lib.json" } }, - "supabase": { - "type": "library", - "root": "libs/supabase", - "entryFile": "index", - "sourceRoot": "libs/supabase/src", + "organization": { + "type": "application", + "root": "apps/organization", + "entryFile": "main", + "sourceRoot": "apps/organization/src", "compilerOptions": { - "tsConfigPath": "libs/supabase/tsconfig.lib.json" + "tsConfigPath": "apps/organization/tsconfig.app.json" } }, - "webhook": { + "platform-service": { "type": "application", - "root": "apps/webhook", + "root": "apps/platform-service", "entryFile": "main", - "sourceRoot": "apps/webhook/src", + "sourceRoot": "apps/platform-service/src", "compilerOptions": { - "tsConfigPath": "apps/webhook/tsconfig.app.json" + "tsConfigPath": "apps/platform-service/tsconfig.app.json" } }, - "aws": { + "prisma": { "type": "library", - "root": "libs/aws", + "root": "libs/prisma", "entryFile": "index", - "sourceRoot": "libs/aws/src", + "sourceRoot": "libs/prisma/src", "compilerOptions": { - "tsConfigPath": "libs/aws/tsconfig.lib.json" + "tsConfigPath": "libs/prisma/tsconfig.lib.json" } }, - "utility": { - "type": "application", - "root": "apps/utility", - "entryFile": "main", - "sourceRoot": "apps/utility/src", + "prisma-service": { + "type": "library", + "root": "libs/prisma-service", + "entryFile": "index", + "sourceRoot": "libs/prisma-service/src", "compilerOptions": { - "tsConfigPath": "apps/utility/tsconfig.app.json" + "tsConfigPath": "libs/prisma-service/tsconfig.lib.json" } }, - "notification": { - "type": "application", - "root": "apps/notification", - "entryFile": "main", - "sourceRoot": "apps/notification/src", + "repositories": { + "type": "library", + "root": "libs/repositories", + "entryFile": "index", + "sourceRoot": "libs/repositories/src", "compilerOptions": { - "tsConfigPath": "apps/notification/tsconfig.app.json" + "tsConfigPath": "libs/repositories/tsconfig.lib.json" } }, - "geo-location": { - "type": "application", - "root": "apps/geo-location", - "entryFile": "main", - "sourceRoot": "apps/geo-location/src", + "supabase": { + "type": "library", + "root": "libs/supabase", + "entryFile": "index", + "sourceRoot": "libs/supabase/src", "compilerOptions": { - "tsConfigPath": "apps/geo-location/tsconfig.app.json" + "tsConfigPath": "libs/supabase/tsconfig.lib.json" } }, - "cloud-wallet": { + "user": { "type": "application", - "root": "apps/cloud-wallet", + "root": "apps/user", "entryFile": "main", - "sourceRoot": "apps/cloud-wallet/src", + "sourceRoot": "apps/user/src", "compilerOptions": { - "tsConfigPath": "apps/cloud-wallet/tsconfig.app.json" + "tsConfigPath": "apps/user/tsconfig.app.json" } }, - "config": { + "user-activity": { "type": "library", - "root": "libs/config", + "root": "libs/user-activity", "entryFile": "index", - "sourceRoot": "libs/config/src", + "sourceRoot": "libs/user-activity/src", "compilerOptions": { - "tsConfigPath": "libs/config/tsconfig.lib.json" + "tsConfigPath": "libs/user-activity/tsconfig.lib.json" } }, - "context": { + "user-org-roles": { "type": "library", - "root": "libs/context", + "root": "libs/user-org-roles", "entryFile": "index", - "sourceRoot": "libs/context/src", + "sourceRoot": "libs/user-org-roles/src", "compilerOptions": { - "tsConfigPath": "libs/context/tsconfig.lib.json" + "tsConfigPath": "libs/user-org-roles/tsconfig.lib.json" } }, - "logger": { + "user-request": { "type": "library", - "root": "libs/logger", + "root": "libs/user-request", "entryFile": "index", - "sourceRoot": "libs/logger/src", + "sourceRoot": "libs/user-request/src", "compilerOptions": { - "tsConfigPath": "libs/logger/tsconfig.lib.json" + "tsConfigPath": "libs/user-request/tsconfig.lib.json" } }, - "oid4vc-issuance": { + "utility": { "type": "application", - "root": "apps/oid4vc-issuance", + "root": "apps/utility", "entryFile": "main", - "sourceRoot": "apps/oid4vc-issuance/src", + "sourceRoot": "apps/utility/src", "compilerOptions": { - "tsConfigPath": "apps/oid4vc-issuance/tsconfig.app.json" + "tsConfigPath": "apps/utility/tsconfig.app.json" + } + }, + "verification": { + "type": "application", + "root": "apps/verification", + "entryFile": "main", + "sourceRoot": "apps/verification/src", + "compilerOptions": { + "tsConfigPath": "apps/verification/tsconfig.app.json" + } + }, + "webhook": { + "type": "application", + "root": "apps/webhook", + "entryFile": "main", + "sourceRoot": "apps/webhook/src", + "compilerOptions": { + "tsConfigPath": "apps/webhook/tsconfig.app.json" } }, - "x509": { + "x509": { "type": "application", "root": "apps/x509", "entryFile": "main", diff --git a/tsconfig.json b/tsconfig.json index d0da5851f..48e1b3c98 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,17 +2,11 @@ "extends": "./tsconfig.build.json", "compilerOptions": { "paths": { - "@credebl/common": [ - "libs/common/src" - ], - "@credebl/common/*": [ - "libs/common/src/*" - ], - "@credebl/keycloak-url": [ - "libs/keycloak-url/src" + "@credebl/aws": [ + "libs/aws/src" ], - "@credebl/keycloak-url/*": [ - "libs/keycloak-url/src/*" + "@credebl/aws/*": [ + "libs/aws/src/*" ], "@credebl/client-registration": [ "libs/client-registration/src" @@ -20,23 +14,35 @@ "@credebl/client-registration/*": [ "libs/client-registration/src/*" ], - "@credebl/prisma": [ - "libs/prisma/src" + "@credebl/common": [ + "libs/common/src" ], - "@credebl/prisma/*": [ - "libs/prisma/src/*" + "@credebl/common/*": [ + "libs/common/src/*" ], - "@credebl/repositories": [ - "libs/repositories/src" + "@credebl/config": [ + "libs/config/src" ], - "@credebl/repositories/*": [ - "libs/repositories/src/*" + "@credebl/config/*": [ + "libs/config/src/*" ], - "@credebl/user-request": [ - "libs/user-request/src" + "@credebl/context": [ + "libs/context/src" ], - "@credebl/user-request/*": [ - "libs/user-request/src/*" + "@credebl/context/*": [ + "libs/context/src/*" + ], + "@credebl/enum": [ + "libs/enum/src" + ], + "@credebl/enum/*": [ + "libs/enum/src/*" + ], + "@credebl/keycloak-url": [ + "libs/keycloak-url/src" + ], + "@credebl/keycloak-url/*": [ + "libs/keycloak-url/src/*" ], "@credebl/logger": [ "libs/logger/src" @@ -44,11 +50,14 @@ "@credebl/logger/*": [ "libs/logger/src/*" ], - "@credebl/enum": [ - "libs/enum/src" + "@credebl/org-roles": [ + "libs/org-roles/src" ], - "@credebl/enum/*": [ - "libs/enum/src/*" + "@credebl/org-roles/*": [ + "libs/org-roles/src/*" + ], + "@credebl/prisma": [ + "libs/prisma/src" ], "@credebl/prisma-service": [ "libs/prisma-service/src" @@ -56,17 +65,20 @@ "@credebl/prisma-service/*": [ "libs/prisma-service/src/*" ], - "@credebl/org-roles": [ - "libs/org-roles/src" + "@credebl/prisma/*": [ + "libs/prisma/src/*" ], - "@credebl/org-roles/*": [ - "libs/org-roles/src/*" + "@credebl/repositories": [ + "libs/repositories/src" ], - "@credebl/user-org-roles": [ - "libs/user-org-roles/src" + "@credebl/repositories/*": [ + "libs/repositories/src/*" ], - "@credebl/user-org-roles/*": [ - "libs/user-org-roles/src/*" + "@credebl/supabase": [ + "libs/supabase/src" + ], + "@credebl/supabase/*": [ + "libs/supabase/src/*" ], "@credebl/user-activity": [ "libs/user-activity/src" @@ -74,35 +86,23 @@ "@credebl/user-activity/*": [ "libs/user-activity/src/*" ], - "@credebl/supabase": [ - "libs/supabase/src" + "@credebl/user-org-roles": [ + "libs/user-org-roles/src" ], - "@credebl/supabase/*": [ - "libs/supabase/src/*" + "@credebl/user-org-roles/*": [ + "libs/user-org-roles/src/*" ], - "@credebl/aws": [ - "libs/aws/src" + "@credebl/user-request": [ + "libs/user-request/src" ], - "@credebl/aws/*": [ - "libs/aws/src/*" + "@credebl/user-request/*": [ + "libs/user-request/src/*" ], "credebl/utility": [ "libs/utility/src" ], "credebl/utility/*": [ "libs/utility/src/*" - ], - "@credebl/config": [ - "libs/config/src" - ], - "@credebl/config/*": [ - "libs/config/src/*" - ], - "@credebl/context": [ - "libs/context/src" - ], - "@credebl/context/*": [ - "libs/context/src/*" ] }, "baseUrl": "./" From 43e918c842951b75feea3fa13ebbff775eba5bd8 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Mon, 3 Nov 2025 13:29:18 +0530 Subject: [PATCH 25/43] refactor: updated CI file Signed-off-by: Tipu_Singh --- .github/workflows/continuous-delivery.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index 8bc6c7bb0..f6ba08bb7 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -31,6 +31,9 @@ jobs: - webhook - organization - seed + - x509 + - oid4vc-issuance + - oid4vc-verification permissions: contents: read From 61f6f6aa70de6cdedc597f3c1993c515b224a5f0 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Mon, 3 Nov 2025 15:40:30 +0530 Subject: [PATCH 26/43] fix: issuance build Signed-off-by: Tipu_Singh --- .../dtos/oid4vc-verifier-presentation.dto.ts | 6 +- apps/issuance/src/issuance.controller.ts | 5 +- .../src/oid4vc-verification.service.ts | 6 +- libs/enum/src/enum.ts | 5 ++ pnpm-lock.yaml | 86 ++++++------------- 5 files changed, 35 insertions(+), 73 deletions(-) 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 e9a6a68cc..802588cd8 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 @@ -14,6 +14,7 @@ import { Validate } from 'class-validator'; import { Type } from 'class-transformer'; +import { RequestSignerMethod } from '@credebl/enum/enum'; /** * DTO for verification-presentation query parameters. @@ -269,11 +270,6 @@ export class DcqlDto { /** * ----------- ROOT DTO ----------- */ -export enum RequestSignerMethod { - DID = 'DID', - X509 = 'X509' -} - /** * Class-level validator: exactly one of the specified properties must be present. */ diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index 5caa8b1ba..abc653b19 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -26,10 +26,7 @@ import { user } from '@prisma/client'; @Controller() export class IssuanceController { private readonly logger = new Logger('IssueCredentialController'); - constructor( - private readonly issuanceService: IssuanceService, - private readonly oidcIssuanceService: OIDCIssuanceService - ) {} + constructor(private readonly issuanceService: IssuanceService) {} @MessagePattern({ cmd: 'get-issuance-records' }) async getIssuanceRecordsByOrgId(payload: { orgId: string; userId: string }): Promise { diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index fb367741b..b15c33271 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -25,6 +25,7 @@ import { map } from 'rxjs'; import { CreateVerifier, UpdateVerifier, VerifierRecord } from '@credebl/common/interfaces/oid4vp-verification'; import { buildUrlWithQuery } from '@credebl/common/cast.helper'; import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfaces'; +import { RequestSignerMethod } from '@credebl/enum/enum'; @Injectable() export class Oid4vpVerificationService { @@ -190,12 +191,13 @@ export class Oid4vpVerificationService { throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); } sessionRequest.verifierId = getVerifierDetails.publicVerifierId; - if ('did' === sessionRequest.requestSigner.method) { + if (RequestSignerMethod.DID === sessionRequest.requestSigner.method) { sessionRequest.requestSigner.didUrl = orgDid; + } else if (RequestSignerMethod.X509 === sessionRequest.requestSigner.method) { + throw new NotFoundException('X509 request signer method not implemented yet'); } const url = await getAgentUrl(agentEndPoint, CommonConstants.OID4VP_VERIFICATION_SESSION); this.logger.log(`[oid4vpCreateVerificationSession] calling agent url: ${url}`); - // console.log('Final sessionRequest', JSON.stringify(sessionRequest, null, 2)); const createdSession = await this._createVerificationSession(sessionRequest, url, orgId); if (!createdSession?.response) { throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 59586a71b..242ef0c5b 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -333,6 +333,11 @@ export enum AttributeType { IMAGE = 'image' } +export enum RequestSignerMethod { + DID = 'did', + X509 = 'x509' +} + // export enum SignerOption { // DID, // X509_P256, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96bc3e38d..2b3b5ecd2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,9 +18,6 @@ catalogs: '@nestjs/cli': specifier: ^11.0.10 version: 11.0.10 - '@nestjs/cli': - specifier: ^11.0.10 - version: 11.0.10 '@nestjs/common': specifier: ^11.1.6 version: 11.1.6 @@ -51,9 +48,6 @@ catalogs: '@nestjs/schematics': specifier: ^11.0.7 version: 11.0.7 - '@nestjs/schematics': - specifier: ^11.0.7 - version: 11.0.7 '@nestjs/swagger': specifier: ^11.2.0 version: 11.2.0 @@ -78,18 +72,6 @@ catalogs: nestjs-typeorm-paginate: specifier: ^4.0.4 version: 4.1.0 - nestjs-cls: - specifier: ^6.0.1 - version: 6.0.1 - nestjs-rate-limiter: - specifier: ^3.1.0 - version: 3.1.0 - nestjs-supabase-auth: - specifier: ^1.0.9 - version: 1.0.9 - nestjs-typeorm-paginate: - specifier: ^4.0.4 - version: 4.1.0 importers: @@ -109,7 +91,7 @@ importers: version: 11.0.3(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(bull@4.16.5) '@nestjs/cache-manager': specifier: 'catalog:' - version: 3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@5.5.3)(rxjs@7.8.2) + version: 3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2) '@nestjs/common': specifier: 'catalog:' version: 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -124,7 +106,7 @@ importers: version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2)) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/passport': specifier: 'catalog:' version: 11.0.5(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.6.0) @@ -142,7 +124,7 @@ importers: version: 11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14) '@nestjs/typeorm': specifier: 'catalog:' - version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))) + version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))) '@nestjs/websockets': specifier: 'catalog:' version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-socket.io@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -294,22 +276,17 @@ importers: specifier: ^2.15.1 version: 2.29.3 nestjs-cls: - specifier: 'catalog:' specifier: 'catalog:' version: 6.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) nestjs-rate-limiter: - specifier: 'catalog:' specifier: 'catalog:' version: 3.1.0 nestjs-supabase-auth: - specifier: 'catalog:' specifier: 'catalog:' version: 1.0.9 nestjs-typeorm-paginate: specifier: 'catalog:' version: 4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))) - specifier: 'catalog:' - version: 4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))) node-html-to-image: specifier: ^4.0.0 version: 4.0.0 @@ -375,7 +352,7 @@ importers: version: 5.0.1(express@4.21.2) typeorm: specifier: ^0.3.10 - version: 0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) + version: 0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) unzipper: specifier: ^0.10.14 version: 0.10.14 @@ -402,15 +379,12 @@ importers: version: 1.6.11 devDependencies: '@nestjs/cli': - specifier: 'catalog:' specifier: 'catalog:' version: 11.0.10(@types/node@20.19.17) '@nestjs/schematics': - specifier: 'catalog:' specifier: 'catalog:' version: 11.0.7(chokidar@4.0.3)(typescript@5.9.2) '@nestjs/testing': - specifier: 'catalog:' specifier: 'catalog:' version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6) '@types/express': @@ -502,7 +476,7 @@ importers: version: 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/testing': specifier: 'catalog:' version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6) @@ -527,7 +501,7 @@ importers: version: 4.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@0.26.1)(rxjs@7.8.2) '@nestjs/cache-manager': specifier: 'catalog:' - version: 3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@5.5.3)(rxjs@7.8.2) + version: 3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2) '@nestjs/common': specifier: 'catalog:' version: 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -536,7 +510,7 @@ importers: version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/swagger': specifier: 'catalog:' version: 11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14) @@ -1213,9 +1187,6 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@keyv/serialize@1.1.1': - resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -4092,8 +4063,8 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - ioredis@5.8.1: - resolution: {integrity: sha512-Qho8TgIamqEPdgiMadJwzRMW3TudIg6vpg4YONokGDudy4eqRIJtDbVX72pfLBcWxvbn3qm/40TyGUObdW4tLQ==} + ioredis@5.8.0: + resolution: {integrity: sha512-AUXbKn9gvo9hHKvk6LbZJQSKn/qIfkWXrnsyL9Yrf+oeXmla9Nmf6XEumOddyhM8neynpK5oAV6r9r99KBuwzA==} engines: {node: '>=12.22.0'} ip-address@10.0.1: @@ -4560,9 +4531,6 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - keyv@5.5.3: - resolution: {integrity: sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==} - klaw@1.3.1: resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} @@ -7770,8 +7738,6 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} - '@keyv/serialize@1.1.1': {} - '@lukeed/csprng@1.1.0': {} '@mapbox/node-pre-gyp@1.0.11': @@ -7829,12 +7795,12 @@ snapshots: bull: 4.16.5 tslib: 2.8.1 - '@nestjs/cache-manager@3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@5.5.3)(rxjs@7.8.2)': + '@nestjs/cache-manager@3.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) cache-manager: 5.7.6 - keyv: 5.5.3 + keyv: 4.5.4 rxjs: 7.8.2 '@nestjs/cli@11.0.10(@types/node@20.19.17)': @@ -7899,7 +7865,7 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/microservices': 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6) '@nestjs/websockets': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-socket.io@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -7917,7 +7883,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.2 - '@nestjs/microservices@11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/microservices@11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -7929,7 +7895,7 @@ snapshots: '@grpc/grpc-js': 1.14.0 '@nestjs/websockets': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-socket.io@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) cache-manager: 5.7.6 - ioredis: 5.8.1 + ioredis: 5.8.0 nats: 2.29.3 '@nestjs/passport@11.0.5(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.6.0)': @@ -8010,16 +7976,16 @@ snapshots: '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/microservices': 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.6(@grpc/grpc-js@1.14.0)(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/websockets@11.1.6)(cache-manager@5.7.6)(ioredis@5.8.0)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6) - '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)))': + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)))': dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.6)(@nestjs/platform-express@11.1.6)(@nestjs/websockets@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2) reflect-metadata: 0.1.14 rxjs: 7.8.2 - typeorm: 0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) + typeorm: 0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) '@nestjs/websockets@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-socket.io@11.1.6)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: @@ -9710,7 +9676,7 @@ snapshots: dependencies: cron-parser: 4.9.0 get-port: 5.1.1 - ioredis: 5.8.1 + ioredis: 5.8.0 lodash: 4.17.21 msgpackr: 1.11.5 semver: 7.7.2 @@ -9727,7 +9693,7 @@ snapshots: cache-manager-ioredis-yet@2.1.2: dependencies: cache-manager: 5.7.6 - ioredis: 5.8.1 + ioredis: 5.8.0 telejson: 7.2.0 transitivePeerDependencies: - supports-color @@ -11335,7 +11301,7 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - ioredis@5.8.1: + ioredis@5.8.0: dependencies: '@ioredis/commands': 1.4.0 cluster-key-slot: 1.1.2 @@ -12015,10 +11981,6 @@ snapshots: dependencies: json-buffer: 3.0.1 - keyv@5.5.3: - dependencies: - '@keyv/serialize': 1.1.1 - klaw@1.3.1: optionalDependencies: graceful-fs: 4.2.11 @@ -12403,10 +12365,10 @@ snapshots: nestjs-supabase-auth@1.0.9: {} - nestjs-typeorm-paginate@4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))): + nestjs-typeorm-paginate@4.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2))): dependencies: '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.14)(rxjs@7.8.2) - typeorm: 0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) + typeorm: 0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)) netmask@2.0.2: {} @@ -13970,7 +13932,7 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.27(ioredis@5.8.1)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)): + typeorm@0.3.27(ioredis@5.8.0)(pg@8.16.3)(redis@3.1.2)(reflect-metadata@0.1.14)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.9.2)): dependencies: '@sqltools/formatter': 1.2.5 ansis: 3.17.0 @@ -13988,7 +13950,7 @@ snapshots: uuid: 11.1.0 yargs: 17.7.2 optionalDependencies: - ioredis: 5.8.1 + ioredis: 5.8.0 pg: 8.16.3 redis: 3.1.2 ts-node: 10.9.2(@types/node@20.19.17)(typescript@5.9.2) From ca3f3c37a871b265c972e7871e3c9bf5f4dee4ce Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Thu, 6 Nov 2025 10:34:14 +0530 Subject: [PATCH 27/43] feat: debug logs (#1505) * feat: debug logs init Signed-off-by: Krishna Waske * fix: add missing debug logs Signed-off-by: Krishna Waske * fix: exend base service for oid4vp Signed-off-by: Krishna Waske * fix: nested response Signed-off-by: Krishna Waske * fix: unwanted description in logs Signed-off-by: Krishna Waske * fix: remove commented code Signed-off-by: Krishna Waske --------- Signed-off-by: Krishna Waske --- .../src/agent-service.controller.ts | 18 ++- .../src/agent-service.service.ts | 29 ++++- .../oid4vc-verification.controller.ts | 55 +++++++- .../oid4vc-verification.module.ts | 2 + .../oid4vc-verification.service.ts | 19 ++- .../src/oid4vc-verification.controller.ts | 25 +++- .../src/oid4vc-verification.repository.ts | 66 +++++++--- .../src/oid4vc-verification.service.ts | 121 ++++++++++++------ 8 files changed, 260 insertions(+), 75 deletions(-) diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index 1e3a914ca..36d132d1e 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -1,4 +1,4 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Logger } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import { AgentServiceService } from './agent-service.service'; import { @@ -36,6 +36,7 @@ import { CreateVerifier, UpdateVerifier } from '@credebl/common/interfaces/oid4v @Controller() export class AgentServiceController { + private readonly logger = new Logger('AgentServiceController'); constructor(private readonly agentServiceService: AgentServiceService) {} /** @@ -420,11 +421,17 @@ export class AgentServiceController { url: string; orgId: string; }): Promise { + this.logger.log( + `[createOid4vpVerifier] Received 'agent-create-oid4vp-verifier' request for orgId=${payload?.orgId || 'N/A'}` + ); return this.agentServiceService.createOid4vpVerifier(payload.verifierDetails, payload.url, payload.orgId); } @MessagePattern({ cmd: 'agent-delete-oid4vp-verifier' }) async deleteOid4vpVerifier(payload: { url: string; orgId: string }): Promise { + this.logger.log( + `[deleteOid4vpVerifier] Received 'agent-delete-oid4vp-verifier' request for orgId=${payload?.orgId || 'N/A'}` + ); return this.agentServiceService.deleteOid4vpVerifier(payload.url, payload.orgId); } @@ -434,11 +441,17 @@ export class AgentServiceController { url: string; orgId: string; }): Promise { + this.logger.log( + `[updateOid4vpVerifier] Received 'agent-update-oid4vp-verifier' request for orgId=${payload?.orgId || 'N/A'}` + ); return this.agentServiceService.updateOid4vpVerifier(payload.verifierDetails, payload.url, payload.orgId); } @MessagePattern({ cmd: 'agent-get-oid4vp-verifier-session' }) async getOid4vpVerifierSession(payload: { url: string; orgId: string }): Promise { + this.logger.log( + `[getOid4vpVerifierSession] Received 'agent-get-oid4vp-verifier-session' request for orgId=${payload?.orgId || 'N/A'}` + ); return this.agentServiceService.getOid4vpVerifierSession(payload.url, payload.orgId); } @@ -448,6 +461,9 @@ export class AgentServiceController { url: string; orgId: string; }): Promise { + this.logger.log( + `[oid4vpCreateVerificationSession] Received 'agent-create-oid4vp-verification-session' request for orgId=${payload?.orgId || 'N/A'}` + ); return this.agentServiceService.createOid4vpVerificationSession(payload.sessionRequest, payload.url, payload.orgId); } } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index fa9f71185..5d3123d3a 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -88,7 +88,7 @@ import { CreateVerifier, UpdateVerifier } from '@credebl/common/interfaces/oid4v @Injectable() @WebSocketGateway() export class AgentServiceService { - private readonly logger = new Logger('WalletService'); + private readonly logger = new Logger('AgentServiceService'); constructor( private readonly agentServiceRepository: AgentServiceRepository, @@ -2273,6 +2273,7 @@ export class AgentServiceService { } async createOid4vpVerifier(verifierDetails: CreateVerifier, url: string, orgId: string): Promise { + this.logger.log(`[createOid4vpVerifier] Creating OID4VP verifier for orgId=${orgId || 'N/A'}`); try { const getApiKey = await this.getOrgAgentApiKey(orgId); const createVerifier = await this.commonService @@ -2280,23 +2281,29 @@ export class AgentServiceService { .then(async (response) => response); return createVerifier; } catch (error) { - this.logger.error(`Error in creating oid4vp verifier in agent service : ${JSON.stringify(error)}`); + this.logger.error( + `[createOid4vpVerifier] Error in creating oid4vp verifier in agent service : ${JSON.stringify(error)}` + ); throw error; } } async deleteOid4vpVerifier(url: string, orgId: string): Promise { + this.logger.log(`[deleteOid4vpVerifier] Deleting OID4VP verifier for orgId=${orgId || 'N/A'}`); try { const getApiKey = await this.getOrgAgentApiKey(orgId); const deleteVerifier = await this.commonService.httpDelete(url, { headers: { authorization: getApiKey } }); return deleteVerifier.data ?? deleteVerifier; } catch (error) { - this.logger.error(`Error in deleting oid4vp verifier in agent service : ${JSON.stringify(error)}`); + this.logger.error( + `[deleteOid4vpVerifier] Error in deleting oid4vp verifier in agent service : ${JSON.stringify(error)}` + ); throw error; } } async updateOid4vpVerifier(verifierDetails: UpdateVerifier, url: string, orgId: string): Promise { + this.logger.log(`[updateOid4vpVerifier] Updating OID4VP verifier for orgId=${orgId || 'N/A'}`); try { const getApiKey = await this.getOrgAgentApiKey(orgId); const updateVerifier = await this.commonService @@ -2304,12 +2311,15 @@ export class AgentServiceService { .then(async (response) => response); return updateVerifier; } catch (error) { - this.logger.error(`Error in updating oid4vp verifier in agent service : ${JSON.stringify(error)}`); + this.logger.error( + `[updateOid4vpVerifier] Error in updating oid4vp verifier in agent service : ${JSON.stringify(error)}` + ); throw error; } } async getOid4vpVerifierSession(url: string, orgId: string): Promise { + this.logger.log(`[getOid4vpVerifierSession] Fetching OID4VP verifier session for orgId=${orgId || 'N/A'}`); try { const agentToken = await this.getOrgAgentApiKey(orgId); const updateVerifier = await this.commonService @@ -2317,11 +2327,16 @@ export class AgentServiceService { .then(async (response) => response); return updateVerifier; } catch (error) { - this.logger.error(`Error in getting oid4vp verifier session in agent service : ${JSON.stringify(error)}`); + this.logger.error( + `[getOid4vpVerifierSession] Error in getting oid4vp verifier session in agent service : ${JSON.stringify(error)}` + ); } } async createOid4vpVerificationSession(sessionRequest: object, url: string, orgId: string): Promise { + this.logger.log( + `[createOid4vpVerificationSession] Creating OID4VP verification session for orgId=${orgId || 'N/A'}` + ); try { const getApiKey = await this.getOrgAgentApiKey(orgId); const createSession = await this.commonService @@ -2329,7 +2344,9 @@ export class AgentServiceService { .then(async (response) => response); return createSession; } catch (error) { - this.logger.error(`Error in creating oid4vp verification session in agent service : ${JSON.stringify(error)}`); + this.logger.error( + `[createOid4vpVerificationSession] Error in creating oid4vp verification session in agent service : ${JSON.stringify(error)}` + ); throw error; } } diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts index f748381b2..6c9edfa2b 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -17,7 +17,8 @@ import { Get, Query, Put, - Delete + Delete, + Logger } from '@nestjs/common'; import { ApiTags, @@ -51,6 +52,8 @@ import { PresentationRequestDto, VerificationPresentationQueryDto } from './dtos @ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) export class Oid4vcVerificationController { + private readonly logger = new Logger('Oid4vpVerificationController'); + constructor(private readonly oid4vcVerificationService: Oid4vcVerificationService) {} /** * Create issuer against a org(tenant) @@ -83,7 +86,12 @@ export class Oid4vcVerificationController { @Body() createVerifier: CreateVerifierDto, @Res() res: Response ): Promise { + this.logger.debug(`[oidcIssuerCreate] Called with orgId=${orgId}, user=${user.id}`); + const createVerifierRes = await this.oid4vcVerificationService.oid4vpCreateVerifier(createVerifier, orgId, user); + + this.logger.debug(`[oidcIssuerCreate] Verifier created: ${createVerifierRes.id}`); + const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.oid4vp.success.create, @@ -125,17 +133,20 @@ export class Oid4vcVerificationController { @Body() updateVerifier: UpdateVerifierDto, @Res() res: Response ): Promise { - console.log('This is updateVerifier', JSON.stringify(updateVerifier, null, 2)); - const createVerifierRes = await this.oid4vcVerificationService.oid4vpUpdateVerifier( + this.logger.debug(`[oidcIssuerUpdate] Called with orgId=${orgId}, verifierId=${verifierId}, user=${user.id}`); + const updateVerifierRes = await this.oid4vcVerificationService.oid4vpUpdateVerifier( updateVerifier, orgId, verifierId, user ); + + this.logger.debug(`[oidcIssuerUpdate] Verifier updated: ${updateVerifierRes.id}`); + const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.oid4vp.success.update, - data: createVerifierRes + data: updateVerifierRes }; return res.status(HttpStatus.CREATED).json(finalResponse); } @@ -178,7 +189,11 @@ export class Oid4vcVerificationController { ) verifierId?: string ): Promise { + this.logger.debug(`[getVerifierDetails] Called with orgId=${orgId}, verifierId=${verifierId}`); + const verifierDetails = await this.oid4vcVerificationService.oid4vpGetVerifier(orgId, verifierId); + + this.logger.debug(`[getVerifierDetails] Result fetched successfully`); const finalResponse: IResponse = { statusCode: HttpStatus.OK, message: ResponseMessages.oid4vp.success.fetch, @@ -217,7 +232,11 @@ export class Oid4vcVerificationController { ) verifierId: string ): Promise { + this.logger.debug(`[deleteVerifierDetails] Called with orgId=${orgId}, verifierId=${verifierId}`); + const verifierDetails = await this.oid4vcVerificationService.oid4vpDeleteVerifier(orgId, verifierId); + + this.logger.debug(`[deleteVerifierDetails] Deleted verifier: ${verifierId}`); const finalResponse: IResponse = { statusCode: HttpStatus.OK, message: ResponseMessages.oid4vp.success.fetch, @@ -262,13 +281,19 @@ export class Oid4vcVerificationController { @Body() createPresentationDto: PresentationRequestDto, @Res() res: Response ): Promise { - console.log('Creating verification presentation with DTO:', JSON.stringify(createPresentationDto, null, 2)); + this.logger.debug( + `[createVerificationPresentation] Called with orgId=${orgId}, verifierId=${verifierId}, user=${user.id}` + ); + const presentation = await this.oid4vcVerificationService.oid4vpCreateVerificationSession( createPresentationDto, orgId, - verifierId, - user + user, + verifierId ); + + this.logger.debug(`[createVerificationPresentation] Presentation created successfully`); + const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.oid4vp.success.create, @@ -305,14 +330,20 @@ export class Oid4vcVerificationController { @Res() res: Response ): Promise { try { + this.logger.debug(`getVerificationPresentation() called with orgId: ${orgId}`); const result = await this.oid4vcVerificationService.oid4vpGetVerifierSession(orgId, query); + this.logger.debug(`Verifier session details fetched successfully for orgId: ${orgId}`); + return res.status(HttpStatus.OK).json({ success: true, message: 'Verifier details retrieved successfully.', data: result }); } catch (error) { + this.logger.debug( + `Error in getVerificationPresentation(): ${error.message || 'Failed to fetch verifier presentation.'}` + ); throw new BadRequestException(error.message || 'Failed to fetch verifier presentation.'); } } @@ -353,17 +384,27 @@ export class Oid4vcVerificationController { @Res() res: Response ): Promise { try { + this.logger.debug( + `getVerificationPresentationResponse() called with orgId: ${orgId}, verificationPresentationId: ${verificationPresentationId}` + ); const result = await this.oid4vcVerificationService.getVerificationSessionResponse( orgId, verificationPresentationId ); + this.logger.debug( + `Verifier presentation response details fetched successfully for verificationPresentationId: ${verificationPresentationId}` + ); + return res.status(HttpStatus.OK).json({ success: true, message: 'Verifier presentation response details retrieved successfully.', data: result }); } catch (error) { + this.logger.debug( + `Error in getVerificationPresentationResponse(): ${error.message || 'Failed to fetch verifier presentation response details.'}` + ); throw new BadRequestException(error.message || 'Failed to fetch verifier presentation response details.'); } } diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts index e956de735..4190f9708 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts @@ -6,10 +6,12 @@ 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 { LoggerModule } from '@credebl/logger'; @Module({ imports: [ HttpModule, + LoggerModule, ClientsModule.register([ { name: 'NATS_CLIENT', diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts index b0244b87a..a2bd17b30 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts @@ -23,6 +23,7 @@ export class Oid4vcVerificationService extends BaseService { // eslint-disable-next-line camelcase ): Promise { const payload = { createVerifier, orgId, userDetails }; + this.logger.debug(`[oid4vpCreateVerifier] Called with orgId=${orgId}, user=${userDetails?.id}`); return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-create', payload); } @@ -34,25 +35,36 @@ export class Oid4vcVerificationService extends BaseService { // eslint-disable-next-line camelcase ): Promise { const payload = { updateVerifier, orgId, verifierId, userDetails }; + this.logger.debug( + `[oid4vpUpdateVerifier] Called with orgId=${orgId}, verifierId=${verifierId}, user=${userDetails?.id}` + ); return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-update', payload); } async oid4vpGetVerifier(orgId, verifierId?: string): Promise { const payload = { orgId, verifierId }; + this.logger.debug(`[oid4vpGetVerifier] Called with orgId=${orgId}, verifierId=${verifierId ?? 'N/A'}`); return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-get', payload); } async oid4vpDeleteVerifier(orgId, verifierId: string): Promise { const payload = { orgId, verifierId }; + this.logger.debug(`[oid4vpDeleteVerifier] Called with orgId=${orgId}, verifierId=${verifierId}`); return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-delete', payload); } async oid4vpGetVerifierSession(orgId, query?: VerificationPresentationQueryDto): Promise { const payload = { orgId, query }; + this.logger.debug( + `[oid4vpGetVerifierSession] Called with orgId=${orgId}, queryParams=${Object.keys(query || {}).length}` + ); return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-session-get', payload); } async getVerificationSessionResponse(orgId, verificationSessionId: string): Promise { const payload = { orgId, verificationSessionId }; + this.logger.debug( + `[getVerificationSessionResponse] Called with orgId=${orgId}, verificationSessionId=${verificationSessionId}` + ); return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verifier-session-response-get', payload); } @@ -60,10 +72,13 @@ export class Oid4vcVerificationService extends BaseService { // eslint-disable-next-line @typescript-eslint/no-explicit-any sessionRequest: any, orgId: string, - verifierId?: string, - userDetails?: user + userDetails: user, + verifierId?: string ): Promise { const payload = { sessionRequest, orgId, verifierId, userDetails }; + this.logger.debug( + `[oid4vpCreateVerificationSession] Called with orgId=${orgId}, verifierId=${verifierId ?? 'N/A'}, user=${userDetails?.id ?? 'N/A'}` + ); return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verification-session-create', payload); } } diff --git a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts index 52a010756..69c73018d 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts @@ -1,4 +1,4 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Logger } from '@nestjs/common'; import { Oid4vpVerificationService } from './oid4vc-verification.service'; import { user } from '@prisma/client'; import { CreateVerifier, UpdateVerifier } from '@credebl/common/interfaces/oid4vp-verification'; @@ -7,7 +7,8 @@ import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfac @Controller() export class Oid4vpVerificationController { - constructor(private readonly oid4vpVerificationService: Oid4vpVerificationService) { } + private readonly logger = new Logger('Oid4vpVerificationController'); + constructor(private readonly oid4vpVerificationService: Oid4vpVerificationService) {} @MessagePattern({ cmd: 'oid4vp-verifier-create' }) async oid4vpCreateVerifier(payload: { @@ -16,6 +17,9 @@ export class Oid4vpVerificationController { userDetails: user; }): Promise { const { createVerifier, orgId, userDetails } = payload; + this.logger.debug( + `[oid4vpCreateVerifier] Received 'oid4vp-verifier-create' request for orgId=${orgId}, user=${userDetails?.id}` + ); return this.oid4vpVerificationService.oid4vpCreateVerifier(createVerifier, orgId, userDetails); } @@ -27,34 +31,48 @@ export class Oid4vpVerificationController { userDetails: user; }): Promise { const { updateVerifier, orgId, verifierId, userDetails } = payload; + this.logger.debug( + `[oid4vpUpdateVerifier] Received 'oid4vp-verifier-update' for orgId=${orgId}, verifierId=${verifierId}, user=${userDetails?.id ?? 'unknown'}` + ); return this.oid4vpVerificationService.oid4vpUpdateVerifier(updateVerifier, orgId, verifierId, userDetails); } @MessagePattern({ cmd: 'oid4vp-verifier-get' }) async oid4vpGetVerifier(payload: { orgId: string; verifierId?: string }): Promise { const { orgId, verifierId } = payload; + this.logger.debug( + `[oid4vpGetVerifier] Received 'oid4vp-verifier-get' for orgId=${orgId}, verifierId=${verifierId ?? 'all'}` + ); return this.oid4vpVerificationService.getVerifierById(orgId, verifierId); } @MessagePattern({ cmd: 'oid4vp-verifier-delete' }) async oid4vpDeleteVerifier(payload: { orgId: string; verifierId: string }): Promise { const { orgId, verifierId } = payload; + this.logger.debug( + `[oid4vpDeleteVerifier]Received 'oid4vp-verifier-delete' for orgId=${orgId}, verifierId=${verifierId}` + ); return this.oid4vpVerificationService.deleteVerifierById(orgId, verifierId); } @MessagePattern({ cmd: 'oid4vp-verifier-session-get' }) async oid4vpGetVerifierSession(payload: { orgId: string; query?: VerificationSessionQuery }): Promise { const { orgId, query } = payload; + this.logger.debug(`[oid4vpGetVerifierSession] Received 'oid4vp-verifier-session-get' for orgId=${orgId}`); return this.oid4vpVerificationService.getVerifierSession(orgId, query); } @MessagePattern({ cmd: 'oid4vp-verifier-session-response-get' }) async getVerificationSessionResponse(payload: { orgId: string; verificationSessionId: string }): Promise { const { orgId, verificationSessionId } = payload; + this.logger.debug( + `[getVerificationSessionResponse] Received 'oid4vp-verifier-session-response-get' for orgId=${orgId}, verificationSessionId=${verificationSessionId}` + ); return this.oid4vpVerificationService.getVerificationSessionResponse(orgId, verificationSessionId); } @MessagePattern({ cmd: 'oid4vp-verification-session-create' }) + // TODO: change name async oid4vpCreateVerificationSession(payload: { orgId: string; verifierId: string; @@ -63,6 +81,9 @@ export class Oid4vpVerificationController { userDetails: user; }): Promise { const { orgId, verifierId, sessionRequest, userDetails } = payload; + this.logger.debug( + `[oid4vpCreateVerificationSession] Received 'oid4vp-verification-session-create' for orgId=${orgId}, verifierId=${verifierId}, user=${userDetails?.id ?? 'unknown'}` + ); return this.oid4vpVerificationService.oid4vpCreateVerificationSession( orgId, verifierId, diff --git a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts index 9d409237b..d49f342bf 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts @@ -8,12 +8,11 @@ import { OrgAgent } from '../interfaces/oid4vp-verifier.interfaces'; @Injectable() export class Oid4vpRepository { - constructor( - private readonly prisma: PrismaService, - private readonly logger: Logger - ) {} + private readonly logger = new Logger('Oid4vpRepository'); + constructor(private readonly prisma: PrismaService) {} async getAgentEndPoint(orgId: string): Promise { + this.logger.debug(`[getAgentEndPoint] called with orgId=${orgId}`); try { const agentDetails = await this.prisma.org_agents.findFirst({ where: { @@ -25,17 +24,20 @@ export class Oid4vpRepository { }); if (!agentDetails) { + this.logger.warn(`[getAgentEndPoint] No agent endpoint found for orgId=${orgId}`); throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } + this.logger.debug(`[getAgentEndPoint] Found agent endpoint with id=${agentDetails.id}`); return agentDetails; } catch (error) { - this.logger.error(`Error in get getAgentEndPoint: ${error.message} `); + this.logger.error(`[getAgentEndPoint] Error in get getAgentEndPoint: ${error.message} `); throw error; } } async getOrgAgentType(orgAgentId: string): Promise { + this.logger.debug(`[getOrgAgentType] called with orgAgentId=${orgAgentId}`); try { const { agent } = await this.prisma.org_agents_type.findFirst({ where: { @@ -43,6 +45,7 @@ export class Oid4vpRepository { } }); + this.logger.debug(`[getOrgAgentType] Found type=${agent}`); return agent; } catch (error) { this.logger.error(`[getOrgAgentType] - error: ${JSON.stringify(error)}`); @@ -51,22 +54,30 @@ export class Oid4vpRepository { } async getOrganizationByTenantId(tenantId: string): Promise { + this.logger.debug(`[getOrganizationByTenantId] called with tenantId=${tenantId}`); try { - return this.prisma.org_agents.findFirst({ + const record = await this.prisma.org_agents.findFirst({ where: { tenantId } }); + this.logger.debug(`[getOrganizationByTenantId] Found orgAgent id=${record?.id ?? 'none'}`); + return record; } catch (error) { - this.logger.error(`Error in getOrganization in issuance repository: ${error.message} `); + this.logger.error( + `[getOrganizationByTenantId] Error in getOrganization in issuance repository: ${error.message} ` + ); throw error; } } async createOid4vpVerifier(verifierDetails, orgId: string, userId: string): Promise { + this.logger.debug( + `[createOid4vpVerifier] called for orgId=${orgId}, userId=${userId}, verifierId=${verifierDetails?.verifierId}` + ); try { const { id, clientMetadata, verifierId } = verifierDetails; - return this.prisma.oid4vp_verifier.create({ + const created = await this.prisma.oid4vp_verifier.create({ data: { metadata: clientMetadata, publicVerifierId: verifierId, @@ -76,16 +87,19 @@ export class Oid4vpRepository { orgAgentId: orgId } }); + this.logger.debug(`[createOid4vpVerifier] Created verifier record with id=${created.id}`); + return created; } catch (error) { - this.logger.error(`Error in createOid4vpVerifier: ${error.message}`); + this.logger.error(`[createOid4vpVerifier] Error in createOid4vpVerifier: ${error?.message ?? error}`); throw error; } } async updateOid4vpVerifier(verifierDetails, userId: string, verifierId: string): Promise { + this.logger.debug(`[updateOid4vpVerifier] called with verifierId=${verifierId}, userId=${userId}`); try { const { clientMetadata } = verifierDetails; - return this.prisma.oid4vp_verifier.update({ + const updated = await this.prisma.oid4vp_verifier.update({ data: { metadata: clientMetadata, lastChangedBy: userId @@ -94,28 +108,34 @@ export class Oid4vpRepository { id: verifierId } }); + this.logger.debug(`[updateOid4vpVerifier] Updated verifier id=${updated.id}`); + return updated; } catch (error) { - this.logger.error(`Error in createOid4vpVerifier: ${error.message}`); + this.logger.error(`[updateOid4vpVerifier] Error in createOid4vpVerifier: ${error.message}`); throw error; } } async getVerifiersByPublicVerifierId(publicVerifierId: string): Promise { + this.logger.debug(`[getVerifiersByPublicVerifierId] called with publicVerifierId=${publicVerifierId}`); try { - return await this.prisma.oid4vp_verifier.findMany({ + const result = await this.prisma.oid4vp_verifier.findMany({ where: { publicVerifierId } }); + this.logger.debug(`[getVerifiersByPublicVerifierId] Found ${result.length} records`); + return result; } catch (error) { - this.logger.error(`Error in getVerifiersByPublicVerifierId: ${error.message}`); + this.logger.error(`[getVerifiersByPublicVerifierId] Error in getVerifiersByPublicVerifierId: ${error.message}`); throw error; } } async getVerifiersByVerifierId(orgId: string, verifierId?: string): Promise { + this.logger.debug(`[getVerifiersByVerifierId] called with orgId=${orgId}, verifierId=${verifierId ?? 'N/A'}`); try { - return await this.prisma.oid4vp_verifier.findMany({ + const result = await this.prisma.oid4vp_verifier.findMany({ where: { id: verifierId, orgAgent: { @@ -123,15 +143,18 @@ export class Oid4vpRepository { } } }); + this.logger.debug(`[getVerifiersByVerifierId] Found ${result.length} records`); + return result; } catch (error) { - this.logger.error(`Error in getVerifiersByPublicVerifierId: ${error.message}`); + this.logger.error(`[getVerifiersByVerifierId] Error in getVerifiersByPublicVerifierId: ${error.message}`); throw error; } } async getVerifierById(orgId: string, verifierId?: string): Promise { + this.logger.debug(`[getVerifierById] called with orgId=${orgId}, verifierId=${verifierId ?? 'N/A'}`); try { - return await this.prisma.oid4vp_verifier.findUnique({ + const result = await this.prisma.oid4vp_verifier.findUnique({ where: { id: verifierId, orgAgent: { @@ -139,15 +162,18 @@ export class Oid4vpRepository { } } }); + this.logger.debug(`[getVerifierById] Found record id=${result?.id ?? 'none'}`); + return result; } catch (error) { - this.logger.error(`Error in getVerifiersByPublicVerifierId: ${error.message}`); + this.logger.error(`[getVerifierById] Error in getVerifiersByPublicVerifierId: ${error.message}`); throw error; } } async deleteVerifierByVerifierId(orgId: string, verifierId?: string): Promise { + this.logger.debug(`[deleteVerifierByVerifierId] called with orgId=${orgId}, verifierId=${verifierId ?? 'N/A'}`); try { - return await this.prisma.oid4vp_verifier.delete({ + const deleted = await this.prisma.oid4vp_verifier.delete({ where: { id: verifierId, orgAgent: { @@ -155,8 +181,10 @@ export class Oid4vpRepository { } } }); + this.logger.debug(`[deleteVerifierByVerifierId] Deleted verifier id=${deleted?.id ?? 'none'}`); + return deleted; } catch (error) { - this.logger.error(`Error in deleteVerifierByVerifierId: ${error.message}`); + this.logger.error(`[deleteVerifierByVerifierId] Error in deleteVerifierByVerifierId: ${error.message}`); throw error; } } diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index b15c33271..88f07e616 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -26,37 +26,44 @@ import { CreateVerifier, UpdateVerifier, VerifierRecord } from '@credebl/common/ import { buildUrlWithQuery } from '@credebl/common/cast.helper'; import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfaces'; import { RequestSignerMethod } from '@credebl/enum/enum'; +import { BaseService } from 'libs/service/base.service'; @Injectable() -export class Oid4vpVerificationService { - private readonly logger = new Logger('IssueCredentialService'); +export class Oid4vpVerificationService extends BaseService { constructor( @Inject('NATS_CLIENT') private readonly oid4vpVerificationServiceProxy: ClientProxy, private readonly oid4vpRepository: Oid4vpRepository - ) {} + ) { + super('Oid4vpVerificationService'); + } async oid4vpCreateVerifier(createVerifier: CreateVerifier, orgId: string, userDetails: user): Promise { + this.logger.debug(`[oid4vpCreateVerifier] called for orgId=${orgId}, user=${userDetails?.id ?? 'unknown'}`); try { let createdVerifierDetails; const { verifierId } = createVerifier; + this.logger.debug(`[oid4vpCreateVerifier] checking if verifierId=${verifierId} already exists`); const checkIdExist = await this.oid4vpRepository.getVerifiersByPublicVerifierId(verifierId); if (0 < checkIdExist.length) { throw new ConflictException(ResponseMessages.oid4vp.error.verifierIdAlreadyExists); } + + this.logger.debug(`[oid4vpCreateVerifier] fetching agent endpoint for orgId=${orgId}`); const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint, id } = agentDetails; const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_CREATE); - console.log('url:::', url); + this.logger.debug(`[oid4vpCreateVerifier] calling agent URL=${url}`); + try { createdVerifierDetails = await this._createOid4vpVerifier(createVerifier, url, orgId); - if (!createdVerifierDetails.response) { + if (!createdVerifierDetails) { throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); } - createdVerifierDetails = createdVerifierDetails.response as VerifierRecord; - console.log('createdVerifierDetails', createdVerifierDetails); + createdVerifierDetails = createdVerifierDetails as VerifierRecord; + this.logger.debug('[oid4vpCreateVerifier] verifier creation response received successfully from agent'); } catch (error) { const status409 = 409 === error?.status?.message?.statusCode || 409 === error?.response?.status || 409 === error?.statusCode; @@ -66,19 +73,22 @@ export class Oid4vpVerificationService { } throw error; } + + this.logger.debug(`[oid4vpCreateVerifier] saving verifier details for orgId=${orgId}`); const saveVerifierDetails = await this.oid4vpRepository.createOid4vpVerifier( createdVerifierDetails, id, userDetails.id ); - console.log('saveVerifierDetails', saveVerifierDetails); if (!saveVerifierDetails) { throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); } + + this.logger.debug(`[oid4vpCreateVerifier] verifier created successfully for orgId=${orgId}`); return saveVerifierDetails; } catch (error) { this.logger.error( - `[oid4vpCreateVerifier] - error in oid4vpCreateVerifier issuance records: ${JSON.stringify(error)}` + `[oid4vpCreateVerifier] - error in oid4vpCreateVerifier issuance records: ${error?.response?.message ?? JSON.stringify(error?.response ?? error)}` ); throw new RpcException(error?.response ?? error); } @@ -92,7 +102,7 @@ export class Oid4vpVerificationService { ): Promise { try { let updatedVerifierDetails; - const existingVerifiers = await this.oid4vpRepository.getVerifiersByVerifierId(verifierId); + const existingVerifiers = await this.oid4vpRepository.getVerifiersByVerifierId(orgId, verifierId); if (0 > existingVerifiers.length) { throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); } @@ -107,13 +117,14 @@ export class Oid4vpVerificationService { CommonConstants.OIDC_VERIFIER_UPDATE, existingVerifiers[0].publicVerifierId ); - console.log('url:::', url); + this.logger.debug(`[oid4vpUpdateVerifier] calling agent URL=${url}`); + try { updatedVerifierDetails = await this._updateOid4vpVerifier(updateVerifier, url, orgId); - if (!updatedVerifierDetails.response) { + if (!updatedVerifierDetails) { throw new InternalServerErrorException(ResponseMessages.oid4vp.error.updateFailed); } - updatedVerifierDetails = updatedVerifierDetails.response.data as VerifierRecord; + updatedVerifierDetails = updatedVerifierDetails.data as VerifierRecord; } catch (error) { // We'll not need this const status409 = @@ -132,6 +143,10 @@ export class Oid4vpVerificationService { if (!updateVerifierDetails) { throw new InternalServerErrorException(ResponseMessages.oid4vp.error.updateFailed); } + + this.logger.debug( + `[oid4vpUpdateVerifier] verifier updated successfully for orgId=${orgId}, verifierId=${verifierId}` + ); return updateVerifierDetails; } catch (error) { this.logger.error(`[oid4vpUpdateVerifier] - error in oid4vpUpdateVerifier records: ${JSON.stringify(error)}`); @@ -140,19 +155,22 @@ export class Oid4vpVerificationService { } async getVerifierById(orgId: string, verifierId?: string): Promise { + this.logger.debug(`[getVerifierById] fetching verifier(s) for orgId=${orgId}, verifierId=${verifierId ?? 'all'}`); try { const verifiers = await this.oid4vpRepository.getVerifiersByVerifierId(orgId, verifierId); if (!verifiers || 0 === verifiers.length) { throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); } + this.logger.debug(`[getVerifierById] ${verifiers.length} record(s) found`); return verifiers; } catch (error) { - this.logger.error(`[getVerifierById] - error: ${JSON.stringify(error)}`); + this.logger.error(`[getVerifierById] - error: ${error?.response ?? error?.message ?? JSON.stringify(error)}`); throw new RpcException(error?.response ?? error); } } async deleteVerifierById(orgId: string, verifierId: string): Promise { + this.logger.debug(`[deleteVerifierById] called for orgId=${orgId}, verifierId=${verifierId}`); try { const checkIdExist = await this.oid4vpRepository.getVerifiersByVerifierId(orgId, verifierId); if (0 == checkIdExist.length) { @@ -165,19 +183,28 @@ export class Oid4vpVerificationService { } const { agentEndPoint, id } = agentDetails; const url = await getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_DELETE, checkIdExist[0].verifierId); - console.log('url:::', url); + this.logger.debug(`[deleteVerifierById] calling agent URL=${url}`); await this._deleteOid4vpVerifier(url, orgId); const verifier = await this.oid4vpRepository.deleteVerifierByVerifierId(orgId, verifierId); + + this.logger.debug( + `[deleteVerifierById] verifier deleted successfully for orgId=${orgId}, verifierId=${verifierId}` + ); return verifier; } catch (error) { - this.logger.error(`[deleteVerifierById] - error: ${JSON.stringify(error)}`); + this.logger.error( + `[deleteVerifierById] - error: ${JSON.stringify(error?.response ?? error?.error ?? error ?? 'Something went wrong')}` + ); throw new RpcException(error?.response ?? error.error ?? error); } } async oid4vpCreateVerificationSession(orgId, verifierId, sessionRequest, userDetails: user): Promise { + this.logger.debug( + `[oid4vpCreateVerificationSession] called for orgId=${orgId}, verifierId=${verifierId}, user=${userDetails?.id ?? 'unknown'}` + ); try { const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); if (!agentDetails) { @@ -197,22 +224,26 @@ export class Oid4vpVerificationService { throw new NotFoundException('X509 request signer method not implemented yet'); } const url = await getAgentUrl(agentEndPoint, CommonConstants.OID4VP_VERIFICATION_SESSION); - this.logger.log(`[oid4vpCreateVerificationSession] calling agent url: ${url}`); + this.logger.debug(`[oid4vpCreateVerificationSession] calling agent URL=${url}`); const createdSession = await this._createVerificationSession(sessionRequest, url, orgId); if (!createdSession?.response) { throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); } + this.logger.debug( + `[oid4vpCreateVerificationSession] verification session created successfully for orgId=${orgId}` + ); return createdSession.response; } catch (error) { this.logger.error( - `[oid4vpCreateVerificationSession] - error creating verification session: ${JSON.stringify(error)}` + `[oid4vpCreateVerificationSession] - error creating verification session: ${JSON.stringify(error?.response ?? error)}` ); throw new RpcException(error?.response ?? error); } } async getVerifierSession(orgId: string, query?: VerificationSessionQuery): Promise { + this.logger.debug(`[getVerifierSession] called for orgId=${orgId}, potentially with a query`); try { const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); if (!agentDetails) { @@ -227,12 +258,13 @@ export class Oid4vpVerificationService { if (!query.id) { url = buildUrlWithQuery(url, query); } - console.log('url:::', url); + this.logger.debug(`[getVerifierSession] calling agent URL=${url}`); const verifiers = await await this._getOid4vpVerifierSession(url, orgId); if (!verifiers || 0 === verifiers.length) { throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); } + this.logger.debug(`[getVerifierSession] ${verifiers.length} verifier session(s) found for orgId=${orgId}`); return verifiers; } catch (error) { this.logger.error(`[getVerifierSession] - error: ${JSON.stringify(error)}`); @@ -241,6 +273,9 @@ export class Oid4vpVerificationService { } async getVerificationSessionResponse(orgId: string, verificationSessionId: string): Promise { + this.logger.debug( + `[getVerificationSessionResponse] called for orgId=${orgId}, verificationSessionId=${verificationSessionId}` + ); try { const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); if (!agentDetails) { @@ -249,24 +284,28 @@ export class Oid4vpVerificationService { const { agentEndPoint, id } = agentDetails; const url = getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_SESSION_GET_BY_ID, verificationSessionId); - console.log('url:::', url); + this.logger.debug(`[getVerificationSessionResponse] calling agent URL=${url}`); const verifiers = await await this._getOid4vpVerifierSession(url, orgId); if (!verifiers || 0 === verifiers.length) { throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); } + this.logger.debug(`[getVerificationSessionResponse] response fetched successfully for orgId=${orgId}`); return verifiers; } catch (error) { - this.logger.error(`[getVerificationSessionResponse] - error: ${JSON.stringify(error)}`); + this.logger.error(`[getVerificationSessionResponse] - error: ${JSON.stringify(error?.response ?? error)}`); throw new RpcException(error?.response ?? error); } } async _createOid4vpVerifier(verifierDetails: CreateVerifier, url: string, orgId: string): Promise { + this.logger.debug(`[_createOid4vpVerifier] sending NATS message for orgId=${orgId}`); try { const pattern = { cmd: 'agent-create-oid4vp-verifier' }; const payload = { verifierDetails, url, orgId }; - return this.natsCall(pattern, payload); + const response = await this.natsCall(pattern, payload); + this.logger.debug(`[_createOid4vpVerifier] NATS response received`); + return response; } catch (error) { this.logger.error( `[_createOID4VPVerifier] [NATS call]- error in create OID4VP Verifier : ${JSON.stringify(error)}` @@ -276,10 +315,13 @@ export class Oid4vpVerificationService { } async _deleteOid4vpVerifier(url: string, orgId: string): Promise { + this.logger.debug(`[_deleteOid4vpVerifier] sending NATS message for orgId=${orgId}`); try { const pattern = { cmd: 'agent-delete-oid4vp-verifier' }; const payload = { url, orgId }; - return this.natsCall(pattern, payload); + const response = await this.natsCall(pattern, payload); + this.logger.debug(`[_deleteOid4vpVerifier] NATS response received`); + return response; } catch (error) { this.logger.error( `[_deleteOid4vpVerifier] [NATS call]- error in delete OID4VP Verifier : ${JSON.stringify(error)}` @@ -289,10 +331,13 @@ export class Oid4vpVerificationService { } async _updateOid4vpVerifier(verifierDetails: UpdateVerifier, url: string, orgId: string): Promise { + this.logger.debug(`[_updateOid4vpVerifier] sending NATS message for orgId=${orgId}`); try { const pattern = { cmd: 'agent-update-oid4vp-verifier' }; const payload = { verifierDetails, url, orgId }; - return this.natsCall(pattern, payload); + const response = await this.natsCall(pattern, payload); + this.logger.debug(`[_updateOid4vpVerifier] NATS response received`); + return response; } catch (error) { this.logger.error( `[_updateOid4vpVerifier] [NATS call]- error in update OID4VP Verifier : ${JSON.stringify(error)}` @@ -302,10 +347,13 @@ export class Oid4vpVerificationService { } async _createVerificationSession(sessionRequest: any, url: string, orgId: string): Promise { + this.logger.debug(`[_createVerificationSession] sending NATS message for orgId=${orgId}`); try { const pattern = { cmd: 'agent-create-oid4vp-verification-session' }; const payload = { sessionRequest, url, orgId }; - return this.natsCall(pattern, payload); + const response = await this.natsCall(pattern, payload); + this.logger.debug(`[_createVerificationSession] NATS response received`); + return response; } catch (error) { this.logger.error( `[_createVerificationSession] [NATS call]- error in create verification session : ${JSON.stringify(error)}` @@ -315,10 +363,13 @@ export class Oid4vpVerificationService { } async _getOid4vpVerifierSession(url: string, orgId: string): Promise { + this.logger.debug(`[_getOid4vpVerifierSession] sending NATS message for orgId=${orgId}`); try { const pattern = { cmd: 'agent-get-oid4vp-verifier-session' }; const payload = { url, orgId }; - return this.natsCall(pattern, payload); + const response = await this.natsCall(pattern, payload); + this.logger.debug(`[_getOid4vpVerifierSession] NATS response received`); + return response; } catch (error) { this.logger.error( `[_getOid4vpVerifierSession] [NATS call]- error in get OID4VP Verifier Session : ${JSON.stringify(error)}` @@ -328,10 +379,13 @@ export class Oid4vpVerificationService { } async _getVerificationSessionResponse(url: string, orgId: string): Promise { + this.logger.debug(`[_getVerificationSessionResponse] sending NATS message for orgId=${orgId}`); try { const pattern = { cmd: 'agent-get-oid4vp-verifier-session' }; const payload = { url, orgId }; - return this.natsCall(pattern, payload); + const response = await this.natsCall(pattern, payload); + this.logger.debug(`[_getVerificationSessionResponse] NATS response received`); + return response; } catch (error) { this.logger.error( `[_getVerificationSessionResponse] [NATS call]- error in get OID4VP Verifier Session : ${JSON.stringify(error)}` @@ -340,20 +394,11 @@ export class Oid4vpVerificationService { } } - async natsCall( - pattern: object, - payload: object - ): Promise<{ - response: string; - }> { + async natsCall(pattern: object, payload: object): Promise { try { return this.oid4vpVerificationServiceProxy .send(pattern, payload) - .pipe( - map((response) => ({ - response - })) - ) + .pipe(map((response) => response)) .toPromise() .catch((error) => { this.logger.error(`catch: ${JSON.stringify(error)}`); From ff1ce9b6e4561a659e21139fc76785371a2be590 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Thu, 6 Nov 2025 17:46:29 +0530 Subject: [PATCH 28/43] feat: verification webhook (#1507) * feat: verification webhook implementation Signed-off-by: Tipu_Singh * fix: batch size and nats config Signed-off-by: Tipu_Singh * fix: review comment Signed-off-by: Tipu_Singh --------- Signed-off-by: Tipu_Singh --- .../dtos/oid4vc-issuer-template.dto.ts | 13 ++++--- .../dtos/oid4vp-presentation-wh.dto.ts | 27 +++++++++++++ .../oid4vc-verification.controller.ts | 34 +++++++++++++++-- .../oid4vc-verification.module.ts | 2 +- .../oid4vc-verification.service.ts | 11 ++++++ .../interfaces/oid4vc-issuance.interfaces.ts | 2 +- .../helpers/credential-sessions.builder.ts | 1 - apps/oid4vc-issuance/src/main.ts | 2 +- .../src/oid4vc-issuance.service.ts | 21 ++++------ ...oid4vp-verification-sessions.interfaces.ts | 8 ++++ .../src/oid4vc-verification.controller.ts | 9 +++++ .../src/oid4vc-verification.repository.ts | 38 +++++++++++++++++++ .../src/oid4vc-verification.service.ts | 31 +++++++++++++-- libs/common/src/common.constant.ts | 2 +- libs/common/src/response-messages/index.ts | 21 ++++++++++ .../migration.sql | 21 ++++++++++ libs/prisma-service/prisma/schema.prisma | 36 +++++++++++++----- 17 files changed, 238 insertions(+), 41 deletions(-) create mode 100644 apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts create mode 100644 apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts create mode 100644 libs/prisma-service/prisma/migrations/20251105180717_created_table_oid4vp_presentation/migration.sql 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 5397d6524..ad8d175ba 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 @@ -10,7 +10,9 @@ import { IsArray, ValidateIf, IsEmpty, - ArrayNotEmpty + ArrayNotEmpty, + IsDefined, + NotEquals } from 'class-validator'; import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath, PartialType } from '@nestjs/swagger'; import { Type } from 'class-transformer'; @@ -216,12 +218,11 @@ export class CreateCredentialTemplateDto { @IsString() description?: string; - @ApiProperty({ - description: 'Signer option (did or x509)', - enum: SignerOption, - example: SignerOption.DID - }) + @ApiProperty({ enum: SignerOption, description: 'Signer option type' }) @IsEnum(SignerOption) + @ValidateIf((o) => o.format === CredentialFormat.Mdoc) + @IsDefined({ message: 'signerOption is required when format is Mdoc' }) + @NotEquals(SignerOption.DID, { message: 'signerOption must NOT be DID when format is Mdoc' }) signerOption!: SignerOption; @ApiProperty({ enum: CredentialFormat, description: 'Credential format type' }) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts new file mode 100644 index 000000000..cc8dcc4e6 --- /dev/null +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; +export class Oid4vpPresentationWhDto { + @ApiProperty() + @IsString() + id!: string; + + @ApiProperty() + @IsString() + state!: string; + + @ApiProperty() + @IsString() + authorizationRequestId!: string; + + @ApiProperty() + @IsString() + createdAt!: string; + + @ApiProperty() + @IsString() + updatedAt!: string; + + @ApiProperty() + @IsString() + contextCorrelationId!: string; +} diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts index 6c9edfa2b..a6eaf6453 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -27,14 +27,15 @@ import { ApiBearerAuth, ApiForbiddenResponse, ApiUnauthorizedResponse, - ApiQuery + ApiQuery, + ApiExcludeEndpoint } 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 IResponseType, { IResponse } from '@credebl/common/interfaces/response.interface'; import { User } from '../authz/decorators/user.decorator'; import { ResponseMessages } from '@credebl/common/response-messages'; import { Roles } from '../authz/decorators/roles.decorator'; @@ -46,6 +47,7 @@ import { user } from '@prisma/client'; import { Oid4vcVerificationService } from './oid4vc-verification.service'; import { CreateVerifierDto, UpdateVerifierDto } from './dtos/oid4vc-verifier.dto'; import { PresentationRequestDto, VerificationPresentationQueryDto } from './dtos/oid4vc-verifier-presentation.dto'; +import { Oid4vpPresentationWhDto } from '../oid4vc-issuance/dtos/oid4vp-presentation-wh.dto'; @Controller() @UseFilters(CustomExceptionFilter) @ApiTags('OID4VP') @@ -296,7 +298,7 @@ export class Oid4vcVerificationController { const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, - message: ResponseMessages.oid4vp.success.create, + message: ResponseMessages.oid4vpSession.success.create, data: presentation }; return res.status(HttpStatus.CREATED).json(finalResponse); @@ -408,4 +410,30 @@ export class Oid4vcVerificationController { throw new BadRequestException(error.message || 'Failed to fetch verifier presentation response details.'); } } + /** + * Catch issue credential webhook responses + * @param oid4vpPresentationWhDto The details of the oid4vp presentation webhook + * @param id The ID of the organization + * @param res The response object + * @returns The details of the oid4vp presentation webhook + */ + @Post('wh/:id/openid4vc-verification') + @ApiExcludeEndpoint() + @ApiOperation({ + summary: 'Catch OID4VP presentation states', + description: 'Handles webhook responses for OID4VP presentation states.' + }) + async storePresentationWebhook( + @Body() oid4vpPresentationWhDto: Oid4vpPresentationWhDto, + @Param('id') id: string, + @Res() res: Response + ): Promise { + await this.oid4vcVerificationService.oid4vpPresentationWebhook(oid4vpPresentationWhDto, id); + const finalResponse: IResponseType = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.oid4vpSession.success.webhookReceived, + data: [] + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } } diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts index 4190f9708..b4cbf3344 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.module.ts @@ -16,7 +16,7 @@ import { LoggerModule } from '@credebl/logger'; { name: 'NATS_CLIENT', transport: Transport.NATS, - options: getNatsOptions(CommonConstants.ISSUANCE_SERVICE, process.env.API_GATEWAY_NKEY_SEED) + options: getNatsOptions(CommonConstants.OIDC4VC_VERIFICATION_SERVICE, process.env.API_GATEWAY_NKEY_SEED) } ]) ], diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts index a2bd17b30..3a48e9fd4 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts @@ -6,6 +6,7 @@ import { BaseService } from 'libs/service/base.service'; import { oid4vp_verifier, user } from '@prisma/client'; import { CreateVerifierDto, UpdateVerifierDto } from './dtos/oid4vc-verifier.dto'; import { VerificationPresentationQueryDto } from './dtos/oid4vc-verifier-presentation.dto'; +import { Oid4vpPresentationWhDto } from '../oid4vc-issuance/dtos/oid4vp-presentation-wh.dto'; @Injectable() export class Oid4vcVerificationService extends BaseService { @@ -81,4 +82,14 @@ export class Oid4vcVerificationService extends BaseService { ); return this.natsClient.sendNatsMessage(this.oid4vpProxy, 'oid4vp-verification-session-create', payload); } + + oid4vpPresentationWebhook( + oid4vpPresentationWhDto: Oid4vpPresentationWhDto, + id: string + ): Promise<{ + response: object; + }> { + const payload = { oid4vpPresentationWhDto, id }; + return this.natsClient.sendNats(this.oid4vpProxy, 'webhook-oid4vp-presentation', payload); + } } diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts index eb5a59ad9..e3ba6413e 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts @@ -69,7 +69,7 @@ export interface IssuerInitialConfig { authorizationServerConfigs: AuthorizationServerConfig | {}; accessTokenSignerKeyType: AccessTokenSignerKeyType; dpopSigningAlgValuesSupported: string[]; - batchCredentialIssuance: object; + batchCredentialIssuance?: object; credentialConfigurationsSupported: object; } diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index 089052c44..b2a6feff9 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -349,7 +349,6 @@ function buildMdocCredential( ) { throw new UnprocessableEntityException(`${ResponseMessages.oidcIssuerSession.error.missingValidityInfo}`); } - const certificateDetail = activeCertificateDetails.find((x) => x.certificateBase64 === signerOptions[0].x5c[0]); const validationResult = validateCredentialDatesInCertificateWindow( credentialRequest.validityInfo, diff --git a/apps/oid4vc-issuance/src/main.ts b/apps/oid4vc-issuance/src/main.ts index a76e0de0f..be0701c3d 100644 --- a/apps/oid4vc-issuance/src/main.ts +++ b/apps/oid4vc-issuance/src/main.ts @@ -12,7 +12,7 @@ const logger = new Logger(); async function bootstrap(): Promise { const app = await NestFactory.createMicroservice(Oid4vcIssuanceModule, { transport: Transport.NATS, - options: getNatsOptions(CommonConstants.OIDC4VC_ISSUANCE_SERVICE, process.env.ISSUANCE_NKEY_SEED) + options: getNatsOptions(CommonConstants.OIDC4VC_ISSUANCE_SERVICE, process.env.OIDC4VC_ISSUANCE_NKEY_SEED) }); app.useLogger(app.get(NestjsLoggerServiceAdapter)); app.useGlobalFilters(new HttpExceptionFilter()); diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 921575f02..302d5a69a 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -99,10 +99,14 @@ export class Oid4vcIssuanceService { authorizationServerConfigs: issuerCreation?.authorizationServerConfigs || undefined, accessTokenSignerKeyType, dpopSigningAlgValuesSupported, - batchCredentialIssuance: { - batchSize: batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault - }, - credentialConfigurationsSupported + credentialConfigurationsSupported, + ...(batchCredentialIssuanceSize && 0 < batchCredentialIssuanceSize + ? { + batchCredentialIssuance: { + batchSize: batchCredentialIssuanceSize + } + } + : {}) }; let createdIssuer; try { @@ -694,7 +698,6 @@ export class Oid4vcIssuanceService { return updateCredentialOfferOnAgent.response; } catch (error) { - this.logger.error(`[createOidcCredentialOffer] - error: ${JSON.stringify(error)}`); throw new RpcException(error.response ?? error); } } @@ -967,13 +970,9 @@ export class Oid4vcIssuanceService { credentialOfferPayload, issuedCredentials } = CredentialOfferWebhookPayload ?? {}; - - // ensure we only store credential_configuration_ids in the payload for logging and storage const cfgIds: string[] = Array.isArray(credentialOfferPayload?.credential_configuration_ids) ? credentialOfferPayload.credential_configuration_ids : []; - - // convert issuedCredentials to string[] when schema expects string[] const issuedCredentialsArr: string[] | undefined = Array.isArray(issuedCredentials) && 0 < issuedCredentials.length ? issuedCredentials.map((c: any) => ('string' === typeof c ? c : JSON.stringify(c))) @@ -989,8 +988,6 @@ export class Oid4vcIssuanceService { }; console.log('Storing OID4VC Credential Webhook:', JSON.stringify(sanitized, null, 2)); - - // resolve orgId (unchanged logic) let orgId: string; if ('default' !== contextCorrelationId) { const getOrganizationId = await this.oid4vcIssuanceRepository.getOrganizationByTenantId(contextCorrelationId); @@ -998,8 +995,6 @@ export class Oid4vcIssuanceService { } else { orgId = issuanceSessionId; } - - // hand off to repository for persistence (repository will perform the upsert) const agentDetails = await this.oid4vcIssuanceRepository.storeOidcCredentialDetails( CredentialOfferWebhookPayload, orgId diff --git a/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts b/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts new file mode 100644 index 000000000..86b447c96 --- /dev/null +++ b/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts @@ -0,0 +1,8 @@ +export interface Oid4vpPresentationWh { + id: string; + state: string; + createdAt: string; + updatedAt: string; + contextCorrelationId: string; + authorizationRequestId: string; +} diff --git a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts index 69c73018d..5422e7ff8 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts @@ -4,6 +4,7 @@ import { user } from '@prisma/client'; import { CreateVerifier, UpdateVerifier } from '@credebl/common/interfaces/oid4vp-verification'; import { MessagePattern } from '@nestjs/microservices'; import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfaces'; +import { Oid4vpPresentationWh } from '../interfaces/oid4vp-verification-sessions.interfaces'; @Controller() export class Oid4vpVerificationController { @@ -91,4 +92,12 @@ export class Oid4vpVerificationController { userDetails ); } + + @MessagePattern({ cmd: 'webhook-oid4vp-presentation' }) + async oid4vpPresentationWebhook(payload: { + oid4vpPresentationWhDto: Oid4vpPresentationWh; + id: string; + }): Promise { + return this.oid4vpVerificationService.oid4vpPresentationWebhook(payload.oid4vpPresentationWhDto, payload.id); + } } diff --git a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts index d49f342bf..3c0a0bb20 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts @@ -5,6 +5,7 @@ import { oid4vp_verifier, org_agents } from '@prisma/client'; import { PrismaService } from '@credebl/prisma-service'; import { ResponseMessages } from '@credebl/common/response-messages'; import { OrgAgent } from '../interfaces/oid4vp-verifier.interfaces'; +import { Oid4vpPresentationWh } from '../interfaces/oid4vp-verification-sessions.interfaces'; @Injectable() export class Oid4vpRepository { @@ -188,4 +189,41 @@ export class Oid4vpRepository { throw error; } } + + async storeOid4vpPresentationDetails( + oid4vpPresentationPayload: Oid4vpPresentationWh, + orgId: string + ): Promise { + try { + const { + state, + id: verificationSessionId, + contextCorrelationId, + authorizationRequestId + } = oid4vpPresentationPayload; + const credentialDetails = await this.prisma.oid4vp_presentations.upsert({ + where: { + verificationSessionId + }, + update: { + lastChangedBy: orgId, + state + }, + create: { + lastChangedBy: orgId, + createdBy: orgId, + state, + orgId, + contextCorrelationId, + verificationSessionId, + presentationId: authorizationRequestId + } + }); + + return credentialDetails; + } catch (error) { + this.logger.error(`Error in storeOid4vpPresentationDetails in oid4vp-presentation repository: ${error.message} `); + throw error; + } + } } diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index 88f07e616..77e7f9b5c 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -27,6 +27,7 @@ import { buildUrlWithQuery } from '@credebl/common/cast.helper'; import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfaces'; import { RequestSignerMethod } from '@credebl/enum/enum'; import { BaseService } from 'libs/service/base.service'; +import { Oid4vpPresentationWh } from '../interfaces/oid4vp-verification-sessions.interfaces'; @Injectable() export class Oid4vpVerificationService extends BaseService { @@ -282,10 +283,11 @@ export class Oid4vpVerificationService extends BaseService { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint, id } = agentDetails; - - const url = getAgentUrl(agentEndPoint, CommonConstants.OIDC_VERIFIER_SESSION_GET_BY_ID, verificationSessionId); - this.logger.debug(`[getVerificationSessionResponse] calling agent URL=${url}`); - + const url = getAgentUrl( + agentEndPoint, + CommonConstants.OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID, + verificationSessionId + ); const verifiers = await await this._getOid4vpVerifierSession(url, orgId); if (!verifiers || 0 === verifiers.length) { throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); @@ -298,6 +300,27 @@ export class Oid4vpVerificationService extends BaseService { } } + async oid4vpPresentationWebhook(oid4vpPresentation: Oid4vpPresentationWh, id: string): Promise { + try { + const { contextCorrelationId } = oid4vpPresentation ?? {}; + let orgId: string; + if ('default' !== contextCorrelationId) { + const getOrganizationId = await this.oid4vpRepository.getOrganizationByTenantId(contextCorrelationId); + if (!getOrganizationId) { + throw new NotFoundException(ResponseMessages.organisation.error.notFound); + } + orgId = getOrganizationId?.orgId; + } else { + orgId = id; + } + const agentDetails = await this.oid4vpRepository.storeOid4vpPresentationDetails(oid4vpPresentation, orgId); + return agentDetails; + } catch (error) { + this.logger.error(`[storeOid4vpPresentationWebhook] - error: ${JSON.stringify(error)}`); + throw error; + } + } + async _createOid4vpVerifier(verifierDetails: CreateVerifier, url: string, orgId: string): Promise { this.logger.debug(`[_createOid4vpVerifier] sending NATS message for orgId=${orgId}`); try { diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 201a1817b..2afeb5355 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -133,7 +133,7 @@ export enum CommonConstants { URL_OIDC_VERIFIER_GET = '/openid4vc/verifier/#', URL_OIDC_VERIFIER_SESSION_GET_BY_ID = '/openid4vc/verification-sessions/#', URL_OIDC_VERIFIER_SESSION_GET_BY_QUERY = '/openid4vc/verification-sessions', - URL_OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID = '/openid4vc/verification-sessions/response#', + URL_OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID = '/openid4vc/verification-sessions/response/#', URL_OID4VP_VERIFICATION_SESSION = '/openid4vc/verification-sessions/create-presentation-request', //X509 agent API URLs diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 6a56850ed..54e259a3c 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -577,6 +577,27 @@ export const ResponseMessages = { deleteTemplate: 'Error while deleting template.' } }, + oid4vpSession: { + success: { + create: 'OID4VP session verifier created successfully.', + update: 'OID4VP session verifier updated successfully.', + delete: 'OID4VP session verifier deleted successfully.', + fetch: 'OID4VP session verifier(s) fetched successfully.', + getById: 'OID4VP session verifier details fetched successfully.', + webhookReceived: 'OID4VP presentation webhook stored successfully.' + }, + error: { + notFound: 'OID4VP session verifier not found.', + invalidId: 'Invalid OID4VP session verifier ID.', + createFailed: 'Failed to create OID4VP session verifier.', + updateFailed: 'Failed to update OID4VP session verifier.', + deleteFailed: 'Failed to delete OID4VP session verifier.', + notFoundIssuerDisplay: 'Issuer display not found.', + notFoundIssuerDetails: 'Issuer details not found.', + verifierIdAlreadyExists: 'Verifier ID already exists for this verifier.', + deleteTemplate: 'Error while deleting template.' + } + }, x509: { success: { create: 'x509 certificate created successfully', diff --git a/libs/prisma-service/prisma/migrations/20251105180717_created_table_oid4vp_presentation/migration.sql b/libs/prisma-service/prisma/migrations/20251105180717_created_table_oid4vp_presentation/migration.sql new file mode 100644 index 000000000..1ba8f23c5 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251105180717_created_table_oid4vp_presentation/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE "oid4vp_presentations" ( + "id" UUID NOT NULL, + "orgId" UUID NOT NULL, + "verificationSessionId" TEXT NOT NULL, + "presentationId" TEXT NOT NULL, + "state" TEXT NOT NULL, + "contextCorrelationId" TEXT NOT NULL, + "createdBy" UUID NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" UUID NOT NULL, + + CONSTRAINT "oid4vp_presentations_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "oid4vp_presentations_verificationSessionId_key" ON "oid4vp_presentations"("verificationSessionId"); + +-- AddForeignKey +ALTER TABLE "oid4vp_presentations" ADD CONSTRAINT "oid4vp_presentations_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organisation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 1a95f1e6a..df567d673 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -152,6 +152,7 @@ model organisation { credential_definition credential_definition[] file_upload file_upload[] oid4vc_credentials oid4vc_credentials[] + oid4vp_presentations oid4vp_presentations[] } model org_invitations { @@ -588,16 +589,16 @@ model oidc_issuer { } model oid4vc_credentials { - id String @id @default(uuid()) @db.Uuid - orgId String @db.Uuid - issuanceSessionId String @unique - credentialOfferId String - state String - contextCorrelationId String - createdBy String @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedBy String @db.Uuid + id String @id @default(uuid()) @db.Uuid + orgId String @db.Uuid + issuanceSessionId String @unique + credentialOfferId String + state String + contextCorrelationId String + createdBy String @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid credentialConfigurationIds String[] issuedCredentials String[] @@ -606,6 +607,20 @@ model oid4vc_credentials { @@index([credentialConfigurationIds], type: Gin) } +model oid4vp_presentations { + id String @id @default(uuid()) @db.Uuid + orgId String @db.Uuid + verificationSessionId String @unique + presentationId String + state String + contextCorrelationId String + createdBy String @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + organisation organisation @relation(fields: [orgId], references: [id]) +} + enum SignerOption { DID X509_P256 @@ -661,5 +676,6 @@ model oid4vp_verifier { lastChangedBy String @default("1") orgAgentId String @db.Uuid orgAgent org_agents @relation(fields: [orgAgentId], references: [id]) + @@index([orgAgentId]) } From 76350ed8a8de89c5694c6630ba4d50a9c30a1b8a Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Fri, 7 Nov 2025 16:47:52 +0530 Subject: [PATCH 29/43] refactor: webhook logic Signed-off-by: Tipu_Singh --- .../dtos/oid4vc-credential-wh.dto.ts | 4 ++++ .../dtos/oid4vp-presentation-wh.dto.ts | 4 ++++ .../interfaces/oid4vc-wh-interfaces.ts | 19 +++++++++++++++++ .../src/oid4vc-issuance.repository.ts | 21 +++++-------------- ...oid4vp-verification-sessions.interfaces.ts | 1 + .../src/oid4vc-verification.repository.ts | 6 ++++-- .../src/oid4vc-verification.service.ts | 5 ++--- 7 files changed, 39 insertions(+), 21 deletions(-) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts index 54e4a8406..4bbc86f1a 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts @@ -50,6 +50,10 @@ export class OidcIssueCredentialDto { @ApiProperty() @IsString() contextCorrelationId!: string; + + @ApiProperty() + @IsString() + issuerId!: string; } /** diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts index cc8dcc4e6..a1c75fd91 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts @@ -24,4 +24,8 @@ export class Oid4vpPresentationWhDto { @ApiProperty() @IsString() contextCorrelationId!: string; + + @ApiProperty() + @IsString() + verifierId!: string; } diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts index 1e48e4c0f..3bbeb3ac2 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts @@ -9,6 +9,7 @@ export interface Oid4vcCredentialOfferWebhookPayload { }; state?: string; contextCorrelationId?: string; + issuerId?: string; } export interface CredentialPayload { @@ -22,3 +23,21 @@ export interface CredentialPayload { state: string; credentialExchangeId: string; } + +export interface OidcIssueCredentialPayload { + oidcIssueCredentialDto: { + id: string; + credentialOfferId?: string; + state?: string; + contextCorrelationId?: string; + credentialConfigurationIds?: string[]; + issuedCredentials?: string[]; + issuerId?: string; + credentialOfferPayload?: { + credential_issuer?: string; + credential_configuration_ids?: string[]; + grants?: Record; + credentials?: string[]; + }; + }; +} diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts index 959cccb56..a55b71664 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.repository.ts @@ -65,29 +65,17 @@ export class Oid4vcIssuanceRepository { } } - async storeOidcCredentialDetails( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - credentialPayload: - | { - id: string; - credentialOfferId?: string; - state?: string; - contextCorrelationId?: string; - credentialConfigurationIds?: string[]; - issuedCredentials?: string[]; - } - | any, - orgId: string - ): Promise { + async storeOidcCredentialDetails(credentialPayload, orgId: string): Promise { try { - const payload = credentialPayload?.oidcIssueCredentialDto ?? credentialPayload ?? {}; + const payload = credentialPayload.oidcIssueCredentialDto; const { credentialOfferId, state, id: issuanceSessionId, contextCorrelationId, credentialOfferPayload, - issuedCredentials + issuedCredentials, + issuerId } = payload; const credentialDetails = await this.prisma.oid4vc_credentials.upsert({ @@ -108,6 +96,7 @@ export class Oid4vcIssuanceRepository { credentialOfferId, contextCorrelationId, issuanceSessionId, + publicIssuerId: issuerId, credentialConfigurationIds: credentialOfferPayload.credential_configuration_ids ?? [], ...(issuedCredentials !== undefined ? { issuedCredentials } : {}) } diff --git a/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts b/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts index 86b447c96..3cbfca3eb 100644 --- a/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts +++ b/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts @@ -5,4 +5,5 @@ export interface Oid4vpPresentationWh { updatedAt: string; contextCorrelationId: string; authorizationRequestId: string; + verifierId: string; } diff --git a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts index 3c0a0bb20..6cdf3372c 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts @@ -199,7 +199,8 @@ export class Oid4vpRepository { state, id: verificationSessionId, contextCorrelationId, - authorizationRequestId + authorizationRequestId, + verifierId } = oid4vpPresentationPayload; const credentialDetails = await this.prisma.oid4vp_presentations.upsert({ where: { @@ -216,7 +217,8 @@ export class Oid4vpRepository { orgId, contextCorrelationId, verificationSessionId, - presentationId: authorizationRequestId + presentationId: authorizationRequestId, + publicVerifierId: verifierId } }); diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index 77e7f9b5c..5afb46fa0 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -227,14 +227,13 @@ export class Oid4vpVerificationService extends BaseService { const url = await getAgentUrl(agentEndPoint, CommonConstants.OID4VP_VERIFICATION_SESSION); this.logger.debug(`[oid4vpCreateVerificationSession] calling agent URL=${url}`); const createdSession = await this._createVerificationSession(sessionRequest, url, orgId); - if (!createdSession?.response) { + if (!createdSession) { throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); } - this.logger.debug( `[oid4vpCreateVerificationSession] verification session created successfully for orgId=${orgId}` ); - return createdSession.response; + return createdSession; } catch (error) { this.logger.error( `[oid4vpCreateVerificationSession] - error creating verification session: ${JSON.stringify(error?.response ?? error)}` From 0531a8822d91610fa159be7a180cac25310a5f54 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Fri, 7 Nov 2025 18:39:54 +0530 Subject: [PATCH 30/43] feat: added x509 support Signed-off-by: Tipu_Singh --- ...oid4vp-verification-sessions.interfaces.ts | 14 +++++ .../src/oid4vc-verification.repository.ts | 31 ++++++++++ .../src/oid4vc-verification.service.ts | 56 +++++++++++++++---- libs/enum/src/enum.ts | 8 ++- 4 files changed, 96 insertions(+), 13 deletions(-) diff --git a/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts b/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts index 3cbfca3eb..b6db519a3 100644 --- a/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts +++ b/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts @@ -1,3 +1,5 @@ +import { SignerMethodOption } from '@credebl/enum/enum'; + export interface Oid4vpPresentationWh { id: string; state: string; @@ -7,3 +9,15 @@ export interface Oid4vpPresentationWh { authorizationRequestId: string; verifierId: string; } + +export interface DidSigner { + method: SignerMethodOption.DID; + didUrl: string; +} + +export interface X5cSigner { + method: SignerMethodOption.X5C; + x5c: string[]; +} + +export type RequestSigner = DidSigner | X5cSigner; diff --git a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts index 6cdf3372c..d5d4fa4ca 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.repository.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.repository.ts @@ -6,6 +6,8 @@ import { PrismaService } from '@credebl/prisma-service'; import { ResponseMessages } from '@credebl/common/response-messages'; import { OrgAgent } from '../interfaces/oid4vp-verifier.interfaces'; import { Oid4vpPresentationWh } from '../interfaces/oid4vp-verification-sessions.interfaces'; +import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; @Injectable() export class Oid4vpRepository { @@ -228,4 +230,33 @@ export class Oid4vpRepository { throw error; } } + + async getCurrentActiveCertificate(orgId: string, keyType: x5cKeyType): Promise { + try { + const now = new Date(); + + const certificate = await this.prisma.x509_certificates.findFirst({ + where: { + org_agents: { + orgId + }, + status: x5cRecordStatus.Active, + keyType, + validFrom: { + lte: now + }, + expiry: { + gte: now + } + }, + orderBy: { + createdAt: 'desc' + } + }); + return certificate; + } catch (error) { + this.logger.error(`Error in getCurrentActiveCertificate: ${error.message}`); + throw error; + } + } } diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index 5afb46fa0..8f9de4547 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -7,6 +7,7 @@ /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ import { + BadRequestException, ConflictException, HttpException, Inject, @@ -25,10 +26,10 @@ import { map } from 'rxjs'; import { CreateVerifier, UpdateVerifier, VerifierRecord } from '@credebl/common/interfaces/oid4vp-verification'; import { buildUrlWithQuery } from '@credebl/common/cast.helper'; import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfaces'; -import { RequestSignerMethod } from '@credebl/enum/enum'; +import { RequestSignerMethod, SignerMethodOption, x5cKeyType } from '@credebl/enum/enum'; import { BaseService } from 'libs/service/base.service'; -import { Oid4vpPresentationWh } from '../interfaces/oid4vp-verification-sessions.interfaces'; - +import { Oid4vpPresentationWh, RequestSigner } from '../interfaces/oid4vp-verification-sessions.interfaces'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; @Injectable() export class Oid4vpVerificationService extends BaseService { constructor( @@ -207,29 +208,60 @@ export class Oid4vpVerificationService extends BaseService { `[oid4vpCreateVerificationSession] called for orgId=${orgId}, verifierId=${verifierId}, user=${userDetails?.id ?? 'unknown'}` ); try { + const activeCertificateDetails: X509CertificateRecord[] = []; const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } const { agentEndPoint, orgDid } = agentDetails; - const getVerifierDetails = await this.oid4vpRepository.getVerifierById(orgId, verifierId); - - if (!getVerifierDetails) { + const verifier = await this.oid4vpRepository.getVerifierById(orgId, verifierId); + if (!verifier) { throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); } - sessionRequest.verifierId = getVerifierDetails.publicVerifierId; - if (RequestSignerMethod.DID === sessionRequest.requestSigner.method) { - sessionRequest.requestSigner.didUrl = orgDid; - } else if (RequestSignerMethod.X509 === sessionRequest.requestSigner.method) { - throw new NotFoundException('X509 request signer method not implemented yet'); + + sessionRequest.verifierId = verifier.publicVerifierId; + + let requestSigner: RequestSigner | undefined; + + if (sessionRequest.requestSigner.method === RequestSignerMethod.DID) { + requestSigner = { + method: SignerMethodOption.DID, + didUrl: orgDid + }; + } else if (sessionRequest.requestSigner.method === RequestSignerMethod.X509_P256) { + this.logger.debug('X509_P256 request signer method selected'); + + const activeCertificate = await this.oid4vpRepository.getCurrentActiveCertificate(orgId, x5cKeyType.P256); + this.logger.debug(`activeCertificate=${JSON.stringify(activeCertificate)}`); + + if (!activeCertificate) { + throw new NotFoundException('No active certificate(p256) found for issuer'); + } + + requestSigner = { + method: SignerMethodOption.X5C, // "x5c" + x5c: [activeCertificate.certificateBase64] // array with PEM/DER base64 + }; + + activeCertificateDetails.push(activeCertificate); + } else { + throw new BadRequestException(`Unsupported requestSigner method: ${sessionRequest.requestSigner.method}`); } + + // assign the single object (not an array) + sessionRequest.requestSigner = requestSigner; + + console.log(`[oid4vpCreateVerificationSession] sessionRequest=${JSON.stringify(sessionRequest)}`); + const url = await getAgentUrl(agentEndPoint, CommonConstants.OID4VP_VERIFICATION_SESSION); - this.logger.debug(`[oid4vpCreateVerificationSession] calling agent URL=${url}`); + console.log(`[oid4vpCreateVerificationSession] calling agent URL=${url}`); + const createdSession = await this._createVerificationSession(sessionRequest, url, orgId); if (!createdSession) { throw new InternalServerErrorException(ResponseMessages.oid4vp.error.createFailed); } + this.logger.debug( `[oid4vpCreateVerificationSession] verification session created successfully for orgId=${orgId}` ); diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 242ef0c5b..219a330e5 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -334,8 +334,14 @@ export enum AttributeType { } export enum RequestSignerMethod { + DID = 'DID', + X509_P256 = 'X509_P256', + X509_ED25519 = 'X509_ED25519' +} + +export enum SignerMethodOption { DID = 'did', - X509 = 'x509' + X5C = 'x5c' } // export enum SignerOption { From 3c3d3d2f159b357957a25cc5ce5549e6a96002a3 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Mon, 10 Nov 2025 12:05:14 +0530 Subject: [PATCH 31/43] fix: fixed issues with webhooks and columns Signed-off-by: Rinkal Bhojani --- .../dtos/oid4vc-credential-wh.dto.ts | 12 ++++++-- .../dtos/oid4vp-presentation-wh.dto.ts | 12 ++++++-- .../oid4vc-issuance.controller.ts | 22 +++++++++++++- .../oid4vc-issuance.service.ts | 29 +++++++++++++++++++ .../oid4vc-verification.controller.ts | 20 +++++++++++++ .../oid4vc-verification.service.ts | 29 +++++++++++++++++++ .../interfaces/oid4vc-wh-interfaces.ts | 5 ++++ .../src/oid4vc-issuance.service.ts | 20 +++++++++---- .../migration.sql | 25 ++++++++++++++++ libs/prisma-service/prisma/schema.prisma | 2 ++ 10 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 libs/prisma-service/prisma/migrations/20251108070604_added_public_issuer_id_and_public_verifier_id_in_wehook_data_tables/migration.sql diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts index 4bbc86f1a..e36dbe1ed 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts @@ -1,5 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsObject, IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsArray, IsObject, IsOptional, IsString } from 'class-validator'; export class CredentialOfferPayloadDto { @ApiProperty({ type: [String] }) @@ -54,6 +54,14 @@ export class OidcIssueCredentialDto { @ApiProperty() @IsString() issuerId!: string; + + @ApiPropertyOptional() + @IsOptional() + type: string; + + @ApiPropertyOptional() + @IsOptional() + orgId: string; } /** diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts index a1c75fd91..055f31427 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vp-presentation-wh.dto.ts @@ -1,5 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; export class Oid4vpPresentationWhDto { @ApiProperty() @IsString() @@ -28,4 +28,12 @@ export class Oid4vpPresentationWhDto { @ApiProperty() @IsString() verifierId!: string; + + @ApiPropertyOptional() + @IsOptional() + type: string; + + @ApiPropertyOptional() + @IsOptional() + orgId: string; } diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts index b7ea3892b..e80ba280d 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts @@ -18,7 +18,8 @@ import { Delete, Patch, Query, - Put + Put, + Logger } from '@nestjs/common'; import { ApiTags, @@ -59,6 +60,7 @@ import { @ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) export class Oid4vcIssuanceController { + private readonly logger = new Logger('Oid4vcIssuanceController'); constructor(private readonly oid4vcIssuanceService: Oid4vcIssuanceService) {} /** * Create issuer against a org(tenant) @@ -630,6 +632,9 @@ export class Oid4vcIssuanceController { @Param('id') id: string, @Res() res: Response ): Promise { + if (id && 'default' === oidcIssueCredentialDto.contextCorrelationId) { + oidcIssueCredentialDto.orgId = id; + } // const sanitized = sanitizeOidcIssueCredentialDto(oidcIssueCredentialDto); const getCredentialDetails = await this.oid4vcIssuanceService.oidcIssueCredentialWebhook( oidcIssueCredentialDto, @@ -642,6 +647,21 @@ export class Oid4vcIssuanceController { data: getCredentialDetails }; + const webhookUrl = await this.oid4vcIssuanceService + ._getWebhookUrl(oidcIssueCredentialDto.contextCorrelationId, id) + .catch((error) => { + this.logger.debug(`error in getting webhook url ::: ${JSON.stringify(error)}`); + }); + console.log(`webhookUrl `, webhookUrl); + if (webhookUrl) { + console.log(`Org webhook found `, JSON.stringify(webhookUrl), JSON.stringify(oidcIssueCredentialDto)); + const plainIssuanceDto = JSON.parse(JSON.stringify(oidcIssueCredentialDto)); + + await this.oid4vcIssuanceService._postWebhookResponse(webhookUrl, { data: plainIssuanceDto }).catch((error) => { + this.logger.debug(`error in posting webhook response to webhook url ::: ${JSON.stringify(error)}`); + }); + } + return res.status(HttpStatus.CREATED).json(finalResponse); } } diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts index b4da516ab..01eae5e02 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts @@ -139,4 +139,33 @@ export class Oid4vcIssuanceService extends BaseService { const payload = { oidcIssueCredentialDto, id }; return this.natsClient.sendNats(this.issuanceProxy, 'webhook-oid4vc-issue-credential', payload); } + + async _getWebhookUrl(tenantId?: string, orgId?: string): Promise { + const pattern = { cmd: 'get-webhookurl' }; + const payload = { tenantId, orgId }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.issuanceProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw error; + } + } + + async _postWebhookResponse(webhookUrl: string, data: object): Promise { + const pattern = { cmd: 'post-webhook-response-to-webhook-url' }; + const payload = { webhookUrl, data }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.issuanceProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + + throw error; + } + } } diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts index a6eaf6453..d3c5fd13a 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -428,12 +428,32 @@ export class Oid4vcVerificationController { @Param('id') id: string, @Res() res: Response ): Promise { + oid4vpPresentationWhDto.type = 'Oid4vpPresentation'; + if (id && 'default' === oid4vpPresentationWhDto.contextCorrelationId) { + oid4vpPresentationWhDto.orgId = id; + } + await this.oid4vcVerificationService.oid4vpPresentationWebhook(oid4vpPresentationWhDto, id); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, message: ResponseMessages.oid4vpSession.success.webhookReceived, data: [] }; + + const webhookUrl = await this.oid4vcVerificationService + ._getWebhookUrl(oid4vpPresentationWhDto?.contextCorrelationId, id) + .catch((error) => { + this.logger.debug(`error in getting webhook url ::: ${JSON.stringify(error)}`); + }); + + if (webhookUrl) { + await this.oid4vcVerificationService + ._postWebhookResponse(webhookUrl, { data: oid4vpPresentationWhDto }) + .catch((error) => { + this.logger.debug(`error in posting webhook response to webhook url ::: ${JSON.stringify(error)}`); + }); + } + return res.status(HttpStatus.CREATED).json(finalResponse); } } diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts index 3a48e9fd4..6c6280ffd 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts @@ -92,4 +92,33 @@ export class Oid4vcVerificationService extends BaseService { const payload = { oid4vpPresentationWhDto, id }; return this.natsClient.sendNats(this.oid4vpProxy, 'webhook-oid4vp-presentation', payload); } + + async _getWebhookUrl(tenantId?: string, orgId?: string): Promise { + const pattern = { cmd: 'get-webhookurl' }; + const payload = { tenantId, orgId }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.oid4vpProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw error; + } + } + + async _postWebhookResponse(webhookUrl: string, data: object): Promise { + const pattern = { cmd: 'post-webhook-response-to-webhook-url' }; + const payload = { webhookUrl, data }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.oid4vpProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + + throw error; + } + } } diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts index 3bbeb3ac2..2c0033b2f 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-wh-interfaces.ts @@ -1,4 +1,9 @@ export interface Oid4vcCredentialOfferWebhookPayload { + oidcIssueCredentialDto: Oid4vcCredentialOfferWebhookDto; + id: string; +} + +export interface Oid4vcCredentialOfferWebhookDto { id: string; credentialOfferId?: string; issuedCredentials?: Record[]; diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 302d5a69a..8d9a68969 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -962,14 +962,24 @@ export class Oid4vcIssuanceService { ): Promise { try { // pick fields + let organisationId: string; + const { oidcIssueCredentialDto, id } = CredentialOfferWebhookPayload; + + if ('default' !== oidcIssueCredentialDto?.contextCorrelationId) { + const getOrganizationId = await this.oid4vcIssuanceRepository.getOrganizationByTenantId( + oidcIssueCredentialDto?.contextCorrelationId + ); + organisationId = getOrganizationId?.orgId; + } else { + organisationId = id; + } + const { - credentialOfferId, - state, - id: issuanceSessionId, contextCorrelationId, credentialOfferPayload, - issuedCredentials - } = CredentialOfferWebhookPayload ?? {}; + issuedCredentials, + id: issuanceSessionId + } = oidcIssueCredentialDto ?? {}; const cfgIds: string[] = Array.isArray(credentialOfferPayload?.credential_configuration_ids) ? credentialOfferPayload.credential_configuration_ids : []; diff --git a/libs/prisma-service/prisma/migrations/20251108070604_added_public_issuer_id_and_public_verifier_id_in_wehook_data_tables/migration.sql b/libs/prisma-service/prisma/migrations/20251108070604_added_public_issuer_id_and_public_verifier_id_in_wehook_data_tables/migration.sql new file mode 100644 index 000000000..684e2a590 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251108070604_added_public_issuer_id_and_public_verifier_id_in_wehook_data_tables/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - Added the required column `publicIssuerId` to the `oid4vc_credentials` table without a default value. This is not possible if the table is not empty. + - Added the required column `publicVerifierId` to the `oid4vp_presentations` table without a default value. This is not possible if the table is not empty. + +*/ + +-- AlterTable +-- Add the column first +ALTER TABLE "oid4vc_credentials" +ADD COLUMN "publicIssuerId" TEXT; + +-- Optional: add a temporary default or backfill +UPDATE "oid4vc_credentials" SET "publicIssuerId" = 'default-issuer'; + +-- Make it required if needed +ALTER TABLE "oid4vc_credentials" +ALTER COLUMN "publicIssuerId" SET NOT NULL; + + +-- AlterTable +ALTER TABLE "oid4vp_presentations" ADD COLUMN "publicVerifierId" TEXT; +UPDATE "oid4vp_presentations" SET "publicVerifierId" = 'default-verifier-id'; +ALTER TABLE "oid4vp_presentations" ALTER COLUMN "publicVerifierId" SET NOT NULL; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index df567d673..ac80fd785 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -601,6 +601,7 @@ model oid4vc_credentials { lastChangedBy String @db.Uuid credentialConfigurationIds String[] issuedCredentials String[] + publicIssuerId String organisation organisation @relation(fields: [orgId], references: [id]) @@ -618,6 +619,7 @@ model oid4vp_presentations { createDateTime DateTime @default(now()) @db.Timestamptz(6) lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) lastChangedBy String @db.Uuid + publicVerifierId String organisation organisation @relation(fields: [orgId], references: [id]) } From 8150fc0f1e3bceaf7e1e180040b79f785bd60794 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Mon, 10 Nov 2025 16:54:32 +0530 Subject: [PATCH 32/43] fix: issuance webhook fix Signed-off-by: Tipu_Singh --- apps/oid4vc-issuance/src/oid4vc-issuance.service.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 8d9a68969..5509bf548 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -997,17 +997,9 @@ export class Oid4vcIssuanceService { } }; - console.log('Storing OID4VC Credential Webhook:', JSON.stringify(sanitized, null, 2)); - let orgId: string; - if ('default' !== contextCorrelationId) { - const getOrganizationId = await this.oid4vcIssuanceRepository.getOrganizationByTenantId(contextCorrelationId); - orgId = getOrganizationId?.orgId; - } else { - orgId = issuanceSessionId; - } const agentDetails = await this.oid4vcIssuanceRepository.storeOidcCredentialDetails( CredentialOfferWebhookPayload, - orgId + organisationId ); return agentDetails; } catch (error) { From 3fa80736661d3e568ca0e94c2dfb627fde352902 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Mon, 10 Nov 2025 18:10:21 +0530 Subject: [PATCH 33/43] fix: refactored code and added missing condition for ed25519 in verification request Signed-off-by: Rinkal Bhojani --- .../dtos/oid4vc-verifier-presentation.dto.ts | 9 +++---- .../src/oid4vc-verification.service.ts | 24 ++++++++++++------- libs/enum/src/enum.ts | 12 ---------- 3 files changed, 21 insertions(+), 24 deletions(-) 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 802588cd8..8c553997c 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 @@ -14,7 +14,7 @@ import { Validate } from 'class-validator'; import { Type } from 'class-transformer'; -import { RequestSignerMethod } from '@credebl/enum/enum'; +import { SignerOption } from '@prisma/client'; /** * DTO for verification-presentation query parameters. @@ -298,12 +298,13 @@ export class OnlyOneOfConstraint implements ValidatorConstraintInterface { export class PresentationRequestDto { @ApiPropertyOptional({ example: { - method: 'did' - } + method: 'DID' + }, + description: 'Signer option type' }) @IsOptional() requestSigner?: { - method: RequestSignerMethod; + method: SignerOption; }; @ApiPropertyOptional({ diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index 8f9de4547..cff888e4a 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -21,15 +21,15 @@ import { CommonConstants } from '@credebl/common/common.constant'; import { ResponseMessages } from '@credebl/common/response-messages'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { getAgentUrl } from '@credebl/common/common.utils'; -import { user } from '@prisma/client'; +import { SignerOption, user } from '@prisma/client'; import { map } from 'rxjs'; import { CreateVerifier, UpdateVerifier, VerifierRecord } from '@credebl/common/interfaces/oid4vp-verification'; import { buildUrlWithQuery } from '@credebl/common/cast.helper'; import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfaces'; -import { RequestSignerMethod, SignerMethodOption, x5cKeyType } from '@credebl/enum/enum'; import { BaseService } from 'libs/service/base.service'; import { Oid4vpPresentationWh, RequestSigner } from '../interfaces/oid4vp-verification-sessions.interfaces'; import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; +import { SignerMethodOption } from '@credebl/enum/enum'; @Injectable() export class Oid4vpVerificationService extends BaseService { constructor( @@ -224,19 +224,27 @@ export class Oid4vpVerificationService extends BaseService { let requestSigner: RequestSigner | undefined; - if (sessionRequest.requestSigner.method === RequestSignerMethod.DID) { + if (sessionRequest.requestSigner.method === SignerOption.DID) { requestSigner = { method: SignerMethodOption.DID, didUrl: orgDid }; - } else if (sessionRequest.requestSigner.method === RequestSignerMethod.X509_P256) { - this.logger.debug('X509_P256 request signer method selected'); - - const activeCertificate = await this.oid4vpRepository.getCurrentActiveCertificate(orgId, x5cKeyType.P256); + } else if ( + sessionRequest.requestSigner.method === SignerOption.X509_P256 || + sessionRequest.requestSigner.method === SignerOption.X509_ED25519 + ) { + this.logger.debug('X5C based request signer method selected'); + + const activeCertificate = await this.oid4vpRepository.getCurrentActiveCertificate( + orgId, + sessionRequest.requestSigner.methodv + ); this.logger.debug(`activeCertificate=${JSON.stringify(activeCertificate)}`); if (!activeCertificate) { - throw new NotFoundException('No active certificate(p256) found for issuer'); + throw new NotFoundException( + `No active certificate(${sessionRequest.requestSigner.method}}) found for issuer` + ); } requestSigner = { diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 219a330e5..3893b7411 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -333,19 +333,7 @@ export enum AttributeType { IMAGE = 'image' } -export enum RequestSignerMethod { - DID = 'DID', - X509_P256 = 'X509_P256', - X509_ED25519 = 'X509_ED25519' -} - export enum SignerMethodOption { DID = 'did', X5C = 'x5c' } - -// export enum SignerOption { -// DID, -// X509_P256, -// X509_ED25519 -// } From 2567509695ef15e46d86b1963b46d4046f358af5 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Mon, 10 Nov 2025 19:15:06 +0530 Subject: [PATCH 34/43] hotfix: added missing Nkey for oid4vc-veriifcation service Signed-off-by: Rinkal Bhojani --- .env.demo | 2 ++ .env.sample | 2 ++ apps/oid4vc-verification/src/main.ts | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.env.demo b/.env.demo index afbce9304..89b8d4442 100644 --- a/.env.demo +++ b/.env.demo @@ -103,6 +103,8 @@ CLOUD_WALLET_NKEY_SEED= GEOLOCATION_NKEY_SEED= NOTIFICATION_NKEY_SEED= X509_NKEY_SEED= +OIDC4VC_ISSUANCE_NKEY_SEED= +OIDC4VC_VERIFICATION_NKEY_SEED= KEYCLOAK_DOMAIN=http://localhost:8080/ KEYCLOAK_ADMIN_URL=http://localhost:8080 diff --git a/.env.sample b/.env.sample index 7e5397508..dc022ca6e 100644 --- a/.env.sample +++ b/.env.sample @@ -122,6 +122,8 @@ SCHEMA_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for schema servic UTILITIES_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for utilities service GEOLOCATION_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for geo-location service X509_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for x509 service +OIDC4VC_ISSUANCE_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for x509 service +OIDC4VC_VERIFICATION_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for x509 service AFJ_AGENT_TOKEN_PATH=/apps/agent-provisioning/AFJ/token/ diff --git a/apps/oid4vc-verification/src/main.ts b/apps/oid4vc-verification/src/main.ts index fbb4b7b91..d15ca1544 100644 --- a/apps/oid4vc-verification/src/main.ts +++ b/apps/oid4vc-verification/src/main.ts @@ -12,7 +12,7 @@ const logger = new Logger(); async function bootstrap(): Promise { const app = await NestFactory.createMicroservice(Oid4vpModule, { transport: Transport.NATS, - options: getNatsOptions(CommonConstants.OIDC4VC_VERIFICATION_SERVICE, process.env.Verification_NKEY_SEED) + options: getNatsOptions(CommonConstants.OIDC4VC_VERIFICATION_SERVICE, process.env.OIDC4VC_VERIFICATION_NKEY_SEED) }); app.useLogger(app.get(NestjsLoggerServiceAdapter)); app.useGlobalFilters(new HttpExceptionFilter()); From 94e6bbbdf994e4f78dfe58d7d6a6388c97537adf Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Wed, 12 Nov 2025 13:22:36 +0530 Subject: [PATCH 35/43] feat: trace contextId across services (#1509) * feat: trace contextId across services, including passed from headers of http request Signed-off-by: Krishna Waske * fix: import issues caused by merge Signed-off-by: Krishna Waske * fix: contextId refactoring and coderabbit resolve Signed-off-by: Krishna Waske * fix: make contextInterceptorModule resilient Signed-off-by: Krishna Waske --------- Signed-off-by: Krishna Waske --- .../agent-service/src/agent-service.module.ts | 14 ++-- apps/api-gateway/src/main.ts | 2 - .../oid4vc-verification.controller.ts | 2 +- .../oid4vc-verification.service.ts | 11 ++- apps/oid4vc-verification/src/main.ts | 4 +- .../src/oid4vc-verification.controller.ts | 6 +- .../src/oid4vc-verification.module.ts | 29 +++++--- .../src/oid4vc-verification.service.ts | 69 +++++++++---------- libs/context/src/contextInterceptorModule.ts | 43 +++++++----- libs/context/src/contextModule.ts | 19 ++++- libs/logger/src/logging.interceptor.ts | 14 ++-- 11 files changed, 127 insertions(+), 86 deletions(-) diff --git a/apps/agent-service/src/agent-service.module.ts b/apps/agent-service/src/agent-service.module.ts index d8fe46193..85aa5ad31 100644 --- a/apps/agent-service/src/agent-service.module.ts +++ b/apps/agent-service/src/agent-service.module.ts @@ -1,4 +1,4 @@ -import { CommonModule } from '@credebl/common'; +import { CommonModule, NatsInterceptor } from '@credebl/common'; import { PrismaService } from '@credebl/prisma-service'; import { Logger, Module } from '@nestjs/common'; import { ClientsModule, Transport } from '@nestjs/microservices'; @@ -17,13 +17,15 @@ import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { GlobalConfigModule } from '@credebl/config/global-config.module'; import { ContextInterceptorModule } from '@credebl/context/contextInterceptorModule'; import { NATSClient } from '@credebl/common/NATSClient'; - +import { APP_INTERCEPTOR } from '@nestjs/core'; @Module({ imports: [ ConfigModule.forRoot(), GlobalConfigModule, - LoggerModule, PlatformConfig, ContextInterceptorModule, + LoggerModule, + PlatformConfig, + ContextInterceptorModule, ClientsModule.register([ { name: 'NATS_CLIENT', @@ -47,7 +49,11 @@ import { NATSClient } from '@credebl/common/NATSClient'; provide: MICRO_SERVICE_NAME, useValue: 'Agent-service' }, - NATSClient + NATSClient, + { + provide: APP_INTERCEPTOR, + useClass: NatsInterceptor + } ], exports: [AgentServiceService, AgentServiceRepository, AgentServiceModule] }) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index e92468ce8..2b7239725 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -14,7 +14,6 @@ import { getNatsOptions } from '@credebl/common/nats.config'; import helmet from 'helmet'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { NatsInterceptor } from '@credebl/common'; import { UpdatableValidationPipe } from '@credebl/common/custom-overrideable-validation-pipe'; import * as useragent from 'express-useragent'; @@ -117,7 +116,6 @@ async function bootstrap(): Promise { xssFilter: true }) ); - app.useGlobalInterceptors(new NatsInterceptor()); await app.listen(process.env.API_GATEWAY_PORT, `${process.env.API_GATEWAY_HOST}`); Logger.log(`API Gateway is listening on port ${process.env.API_GATEWAY_PORT}`); } diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts index d3c5fd13a..ce8cb5850 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -54,7 +54,7 @@ import { Oid4vpPresentationWhDto } from '../oid4vc-issuance/dtos/oid4vp-presenta @ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) export class Oid4vcVerificationController { - private readonly logger = new Logger('Oid4vpVerificationController'); + private readonly logger = new Logger('Oid4vcVerificationController'); constructor(private readonly oid4vcVerificationService: Oid4vcVerificationService) {} /** diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts index 6c6280ffd..e319f5674 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts @@ -1,7 +1,6 @@ import { NATSClient } from '@credebl/common/NATSClient'; -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; -import { BaseService } from 'libs/service/base.service'; // eslint-disable-next-line camelcase import { oid4vp_verifier, user } from '@prisma/client'; import { CreateVerifierDto, UpdateVerifierDto } from './dtos/oid4vc-verifier.dto'; @@ -9,13 +8,13 @@ import { VerificationPresentationQueryDto } from './dtos/oid4vc-verifier-present import { Oid4vpPresentationWhDto } from '../oid4vc-issuance/dtos/oid4vp-presentation-wh.dto'; @Injectable() -export class Oid4vcVerificationService extends BaseService { +export class Oid4vcVerificationService { + private readonly logger = new Logger('Oid4vcVerificationService'); + constructor( @Inject('NATS_CLIENT') private readonly oid4vpProxy: ClientProxy, private readonly natsClient: NATSClient - ) { - super('Oid4vcVerificationService'); - } + ) {} async oid4vpCreateVerifier( createVerifier: CreateVerifierDto, diff --git a/apps/oid4vc-verification/src/main.ts b/apps/oid4vc-verification/src/main.ts index d15ca1544..0aec6f280 100644 --- a/apps/oid4vc-verification/src/main.ts +++ b/apps/oid4vc-verification/src/main.ts @@ -1,5 +1,4 @@ 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'; @@ -15,7 +14,8 @@ async function bootstrap(): Promise { options: getNatsOptions(CommonConstants.OIDC4VC_VERIFICATION_SERVICE, process.env.OIDC4VC_VERIFICATION_NKEY_SEED) }); app.useLogger(app.get(NestjsLoggerServiceAdapter)); - app.useGlobalFilters(new HttpExceptionFilter()); + // TODO: Not sure if we want the below + // app.useGlobalFilters(new HttpExceptionFilter()); await app.listen(); logger.log('OID4VC-Verification-Service Microservice is listening to NATS '); diff --git a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts index 5422e7ff8..8f949a41e 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.controller.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.controller.ts @@ -8,8 +8,10 @@ import { Oid4vpPresentationWh } from '../interfaces/oid4vp-verification-sessions @Controller() export class Oid4vpVerificationController { - private readonly logger = new Logger('Oid4vpVerificationController'); - constructor(private readonly oid4vpVerificationService: Oid4vpVerificationService) {} + constructor( + private readonly oid4vpVerificationService: Oid4vpVerificationService, + private logger: Logger + ) {} @MessagePattern({ cmd: 'oid4vp-verifier-create' }) async oid4vpCreateVerifier(payload: { diff --git a/apps/oid4vc-verification/src/oid4vc-verification.module.ts b/apps/oid4vc-verification/src/oid4vc-verification.module.ts index 78b37ac7e..398b3482e 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.module.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.module.ts @@ -4,18 +4,24 @@ import { Oid4vpVerificationService } from './oid4vc-verification.service'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonModule } from '@credebl/common'; -import { CommonConstants } from '@credebl/common/common.constant'; +import { CommonConstants, MICRO_SERVICE_NAME } from '@credebl/common/common.constant'; import { GlobalConfigModule } from '@credebl/config'; import { ContextInterceptorModule } from '@credebl/context'; -import { LoggerModule } from '@credebl/logger'; +import { LoggerModule } from '@credebl/logger/logger.module'; import { CacheModule } from '@nestjs/cache-manager'; import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { NATSClient } from '@credebl/common/NATSClient'; -import { PrismaService } from '@credebl/prisma-service'; +import { PrismaService, PrismaServiceModule } from '@credebl/prisma-service'; import { Oid4vpRepository } from './oid4vc-verification.repository'; +import { ConfigModule } from '@nestjs/config'; @Module({ imports: [ + ConfigModule.forRoot(), + ContextInterceptorModule, + PlatformConfig, + LoggerModule, + CacheModule.register(), ClientsModule.register([ { name: 'NATS_CLIENT', @@ -28,12 +34,19 @@ import { Oid4vpRepository } from './oid4vc-verification.repository'; ]), CommonModule, GlobalConfigModule, - LoggerModule, - PlatformConfig, - ContextInterceptorModule, - CacheModule.register() + PrismaServiceModule ], controllers: [Oid4vpVerificationController], - providers: [Oid4vpVerificationService, Oid4vpRepository, PrismaService, Logger, NATSClient] + providers: [ + Oid4vpVerificationService, + Oid4vpRepository, + PrismaService, + Logger, + NATSClient, + { + provide: MICRO_SERVICE_NAME, + useValue: 'Oid4vc-verification-service' + } + ] }) export class Oid4vpModule {} diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index cff888e4a..de17c4dc9 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -9,11 +9,9 @@ import { BadRequestException, ConflictException, - HttpException, Inject, Injectable, InternalServerErrorException, - Logger, NotFoundException } from '@nestjs/common'; import { Oid4vpRepository } from './oid4vc-verification.repository'; @@ -27,6 +25,8 @@ import { CreateVerifier, UpdateVerifier, VerifierRecord } from '@credebl/common/ import { buildUrlWithQuery } from '@credebl/common/cast.helper'; import { VerificationSessionQuery } from '../interfaces/oid4vp-verifier.interfaces'; import { BaseService } from 'libs/service/base.service'; +import { NATSClient } from '@credebl/common/NATSClient'; + import { Oid4vpPresentationWh, RequestSigner } from '../interfaces/oid4vp-verification-sessions.interfaces'; import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; import { SignerMethodOption } from '@credebl/enum/enum'; @@ -34,6 +34,7 @@ import { SignerMethodOption } from '@credebl/enum/enum'; export class Oid4vpVerificationService extends BaseService { constructor( @Inject('NATS_CLIENT') private readonly oid4vpVerificationServiceProxy: ClientProxy, + private readonly natsClient: NATSClient, private readonly oid4vpRepository: Oid4vpRepository ) { super('Oid4vpVerificationService'); @@ -363,9 +364,12 @@ export class Oid4vpVerificationService extends BaseService { async _createOid4vpVerifier(verifierDetails: CreateVerifier, url: string, orgId: string): Promise { this.logger.debug(`[_createOid4vpVerifier] sending NATS message for orgId=${orgId}`); try { - const pattern = { cmd: 'agent-create-oid4vp-verifier' }; const payload = { verifierDetails, url, orgId }; - const response = await this.natsCall(pattern, payload); + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-create-oid4vp-verifier', + payload + ); this.logger.debug(`[_createOid4vpVerifier] NATS response received`); return response; } catch (error) { @@ -379,9 +383,12 @@ export class Oid4vpVerificationService extends BaseService { async _deleteOid4vpVerifier(url: string, orgId: string): Promise { this.logger.debug(`[_deleteOid4vpVerifier] sending NATS message for orgId=${orgId}`); try { - const pattern = { cmd: 'agent-delete-oid4vp-verifier' }; const payload = { url, orgId }; - const response = await this.natsCall(pattern, payload); + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-delete-oid4vp-verifier', + payload + ); this.logger.debug(`[_deleteOid4vpVerifier] NATS response received`); return response; } catch (error) { @@ -395,9 +402,12 @@ export class Oid4vpVerificationService extends BaseService { async _updateOid4vpVerifier(verifierDetails: UpdateVerifier, url: string, orgId: string): Promise { this.logger.debug(`[_updateOid4vpVerifier] sending NATS message for orgId=${orgId}`); try { - const pattern = { cmd: 'agent-update-oid4vp-verifier' }; const payload = { verifierDetails, url, orgId }; - const response = await this.natsCall(pattern, payload); + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-update-oid4vp-verifier', + payload + ); this.logger.debug(`[_updateOid4vpVerifier] NATS response received`); return response; } catch (error) { @@ -411,9 +421,12 @@ export class Oid4vpVerificationService extends BaseService { async _createVerificationSession(sessionRequest: any, url: string, orgId: string): Promise { this.logger.debug(`[_createVerificationSession] sending NATS message for orgId=${orgId}`); try { - const pattern = { cmd: 'agent-create-oid4vp-verification-session' }; const payload = { sessionRequest, url, orgId }; - const response = await this.natsCall(pattern, payload); + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-create-oid4vp-verification-session', + payload + ); this.logger.debug(`[_createVerificationSession] NATS response received`); return response; } catch (error) { @@ -427,9 +440,12 @@ export class Oid4vpVerificationService extends BaseService { async _getOid4vpVerifierSession(url: string, orgId: string): Promise { this.logger.debug(`[_getOid4vpVerifierSession] sending NATS message for orgId=${orgId}`); try { - const pattern = { cmd: 'agent-get-oid4vp-verifier-session' }; const payload = { url, orgId }; - const response = await this.natsCall(pattern, payload); + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-get-oid4vp-verifier-session', + payload + ); this.logger.debug(`[_getOid4vpVerifierSession] NATS response received`); return response; } catch (error) { @@ -443,9 +459,12 @@ export class Oid4vpVerificationService extends BaseService { async _getVerificationSessionResponse(url: string, orgId: string): Promise { this.logger.debug(`[_getVerificationSessionResponse] sending NATS message for orgId=${orgId}`); try { - const pattern = { cmd: 'agent-get-oid4vp-verifier-session' }; const payload = { url, orgId }; - const response = await this.natsCall(pattern, payload); + const response = await this.natsClient.sendNatsMessage( + this.oid4vpVerificationServiceProxy, + 'agent-get-oid4vp-verifier-session', + payload + ); this.logger.debug(`[_getVerificationSessionResponse] NATS response received`); return response; } catch (error) { @@ -455,26 +474,4 @@ export class Oid4vpVerificationService extends BaseService { throw error; } } - - async natsCall(pattern: object, payload: object): Promise { - try { - return this.oid4vpVerificationServiceProxy - .send(pattern, payload) - .pipe(map((response) => response)) - .toPromise() - .catch((error) => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.message - }, - error.error - ); - }); - } catch (error) { - this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); - throw error; - } - } } diff --git a/libs/context/src/contextInterceptorModule.ts b/libs/context/src/contextInterceptorModule.ts index a80a712f9..e9ff13139 100644 --- a/libs/context/src/contextInterceptorModule.ts +++ b/libs/context/src/contextInterceptorModule.ts @@ -1,14 +1,10 @@ -import { ExecutionContext, Global, Module} from '@nestjs/common'; -import { v4 } from 'uuid'; +import { ExecutionContext, Global, Logger, Module } from '@nestjs/common'; +import { v4 as uuid } from 'uuid'; import { ClsModule } from 'nestjs-cls'; import { ContextStorageServiceKey } from './contextStorageService.interface'; import NestjsClsContextStorageService from './nestjsClsContextStorageService'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isNullUndefinedOrEmpty = (obj: any): boolean => null === obj || obj === undefined || ('object' === typeof obj && 0 === Object.keys(obj).length); - @Global() @Module({ imports: [ @@ -17,17 +13,30 @@ const isNullUndefinedOrEmpty = (obj: any): boolean => null === obj || obj === un interceptor: { mount: true, - generateId: true, - idGenerator: (context: ExecutionContext) => { - const rpcContext = context.switchToRpc().getContext(); - const headers = rpcContext.getHeaders(); - if (!isNullUndefinedOrEmpty(headers)) { - return context.switchToRpc().getContext().getHeaders()['_description']; - } else { - return v4(); + generateId: true, + idGenerator: (context: ExecutionContext) => { + try { + const logger = new Logger('ContextInterceptorModule'); + const rpcContext = context.switchToRpc().getContext(); + const headers = rpcContext.getHeaders() ?? {}; + const contextId = headers.get?.('contextId'); + + if (contextId) { + logger.debug(`[idGenerator] Received contextId in headers: ${contextId}`); + return contextId; + } else { + const uuidGenerated = uuid(); + logger.debug( + '[idGenerator] Did not receive contextId in header, generated new contextId: ', + uuidGenerated + ); + return uuidGenerated; + } + } catch (error) { + // eslint-disable-next-line no-console + console.log('[idGenerator] Error in idGenerator: ', error); } - } - + } } }) ], @@ -40,6 +49,4 @@ const isNullUndefinedOrEmpty = (obj: any): boolean => null === obj || obj === un ], exports: [ContextStorageServiceKey] }) - export class ContextInterceptorModule {} - diff --git a/libs/context/src/contextModule.ts b/libs/context/src/contextModule.ts index 4bfb9e2c3..a03d046d1 100644 --- a/libs/context/src/contextModule.ts +++ b/libs/context/src/contextModule.ts @@ -13,7 +13,24 @@ import NestjsClsContextStorageService from './nestjsClsContextStorageService'; middleware: { mount: true, generateId: true, - idGenerator: (req: Request) => req.headers['x-correlation-id'] ?? v4() + idGenerator: (req: Request) => { + // TODO: Check if we want the x-correlation-id or the correlationId + const contextIdHeader = req.headers['contextid'] ?? req.headers['context-id'] ?? req.headers['contextId']; + const correlationIdHeader = req.headers['x-correlation-id']; + let resolvedContextId = + (Array.isArray(contextIdHeader) ? contextIdHeader[0] : contextIdHeader) ?? + (Array.isArray(correlationIdHeader) ? correlationIdHeader[0] : correlationIdHeader); + + if (resolvedContextId) { + // eslint-disable-next-line no-console + console.log('ContextId received in request headers::::', resolvedContextId); + } else { + resolvedContextId = v4(); + // eslint-disable-next-line no-console + console.log('ContextId not received in request headers, generated a new one::::', resolvedContextId); + } + return resolvedContextId; + } } }) ], diff --git a/libs/logger/src/logging.interceptor.ts b/libs/logger/src/logging.interceptor.ts index 6ac3a0611..74b7db289 100644 --- a/libs/logger/src/logging.interceptor.ts +++ b/libs/logger/src/logging.interceptor.ts @@ -17,26 +17,28 @@ export class LoggingInterceptor implements NestInterceptor { private readonly clsService: ClsService, @Inject(ContextStorageServiceKey) private readonly contextStorageService: ContextStorageService, - @Inject(LoggerKey) private readonly _logger: Logger + @Inject(LoggerKey) private readonly logger: Logger ) {} // eslint-disable-next-line @typescript-eslint/no-explicit-any intercept(context: ExecutionContext, next: CallHandler): Observable { return this.clsService.run(() => { - this._logger.info('In LoggingInterceptor configuration'); + this.logger.info('In LoggingInterceptor configuration'); const rpcContext = context.switchToRpc().getContext(); const headers = rpcContext.getHeaders(); - if (!isNullUndefinedOrEmpty(headers) && headers._description) { - this.contextStorageService.set('x-correlation-id', headers._description); - this.contextStorageService.setContextId(headers._description); + if (!isNullUndefinedOrEmpty(headers)) { + this.logger.debug('We found context Id in header of logger Interceptor', headers.get('contextId')); + this.contextStorageService.setContextId(headers.get('contextId')); } else { const newContextId = v4(); + this.logger.debug('Not found context Id in header of logger Interceptor, generating a new one: ', newContextId); this.contextStorageService.set('x-correlation-id', newContextId); + this.contextStorageService.set('contextId', newContextId); this.contextStorageService.setContextId(newContextId); } return next.handle().pipe( catchError((err) => { - this._logger.error(err); + this.logger.error('[intercept] Error in LoggingInterceptor', err); return throwError(() => err); }) ); From 2ed0396c1a2badc12845222d0c1d62890680d3e4 Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Fri, 14 Nov 2025 17:59:26 +0530 Subject: [PATCH 36/43] feat: optionally enable OIDC modules (#1516) * feat: add optionally enable disable oidc module Signed-off-by: Krishna Waske * chore: add some descriptive logs Signed-off-by: Krishna Waske * fix: update the registration of experimental modules Signed-off-by: Krishna Waske * fix: common function from utils Signed-off-by: Krishna Waske --------- Signed-off-by: Krishna Waske --- .env.demo | 2 ++ .env.sample | 3 ++- apps/api-gateway/src/app.module.ts | 9 +++++---- apps/api-gateway/src/main.ts | 7 +++++++ .../src/oid4vc-issuance/oid4vc-issuance.controller.ts | 1 + .../oid4vc-verification.controller.ts | 1 + libs/common/src/common.utils.ts | 6 ++++++ 7 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.env.demo b/.env.demo index 89b8d4442..edd708ebe 100644 --- a/.env.demo +++ b/.env.demo @@ -144,6 +144,8 @@ ELK_PASSWORD=xxxxxx # ELK user password ORGANIZATION=credebl CONTEXT=platform APP=api +# Default is true too, if nothing is passed +HIDE_EXPERIMENTAL_OIDC_CONTROLLERS=true #Schema-file-server APP_PORT=4000 diff --git a/.env.sample b/.env.sample index dc022ca6e..0ee053975 100644 --- a/.env.sample +++ b/.env.sample @@ -67,7 +67,8 @@ POSTGRES_PORT=5432 POSTGRES_USER='postgres' POSTGRES_PASSWORD='xxxxx' POSTGRES_DATABASE= // Please provide your DB name - +# Default is true too, set to 'false', to view OIDC controllers in openAPI docs +HIDE_EXPERIMENTAL_OIDC_CONTROLLERS=true SENDGRID_API_KEY=xxxxxxxxxxxxxx // Please provide your sendgrid API key FRONT_END_URL=http://localhost:3000 diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 5cc672d0d..3cf5b780a 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -6,7 +6,7 @@ import { AppService } from './app.service'; import { AuthzMiddleware } from './authz/authz.middleware'; import { AuthzModule } from './authz/authz.module'; import { ClientsModule, Transport } from '@nestjs/microservices'; -import { ConfigModule } from '@nestjs/config'; +import { ConditionalModule, ConfigModule } from '@nestjs/config'; import { CredentialDefinitionModule } from './credential-definition/credential-definition.module'; import { FidoModule } from './fido/fido.module'; import { IssuanceModule } from './issuance/issuance.module'; @@ -33,6 +33,7 @@ import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; import { X509Module } from './x509/x509.module'; import { Oid4vpModule } from './oid4vc-verification/oid4vc-verification.module'; +import { shouldLoadOidcModules } from '@credebl/common/common.utils'; @Module({ imports: [ @@ -66,9 +67,9 @@ import { Oid4vpModule } from './oid4vc-verification/oid4vc-verification.module'; CacheModule.register(), GeoLocationModule, CloudWalletModule, - Oid4vcIssuanceModule, - Oid4vpModule, - X509Module + ConditionalModule.registerWhen(Oid4vcIssuanceModule, shouldLoadOidcModules), + ConditionalModule.registerWhen(Oid4vpModule, shouldLoadOidcModules), + ConditionalModule.registerWhen(X509Module, shouldLoadOidcModules) ], controllers: [AppController], providers: [ diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 2b7239725..7c609ac4e 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -118,5 +118,12 @@ async function bootstrap(): Promise { ); await app.listen(process.env.API_GATEWAY_PORT, `${process.env.API_GATEWAY_HOST}`); Logger.log(`API Gateway is listening on port ${process.env.API_GATEWAY_PORT}`); + + if ('true' === (process.env.HIDE_EXPERIMENTAL_OIDC_CONTROLLERS || 'true').trim().toLowerCase()) { + Logger.warn('Hiding experimental OIDC Controllers: OID4VC, OID4VP, x509 in OpenAPI docs'); + Logger.verbose( + "To enable the use of experimental OIDC controllers. Set, 'HIDE_EXPERIMENTAL_OIDC_CONTROLLERS' env variable to false" + ); + } } bootstrap(); diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts index e80ba280d..79df3250d 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts @@ -54,6 +54,7 @@ import { GetAllCredentialOfferDto, UpdateCredentialRequestDto } from './dtos/issuer-sessions.dto'; + @Controller() @UseFilters(CustomExceptionFilter) @ApiTags('OID4VC') diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts index ce8cb5850..a87e373b1 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -48,6 +48,7 @@ import { Oid4vcVerificationService } from './oid4vc-verification.service'; import { CreateVerifierDto, UpdateVerifierDto } from './dtos/oid4vc-verifier.dto'; import { PresentationRequestDto, VerificationPresentationQueryDto } from './dtos/oid4vc-verifier-presentation.dto'; import { Oid4vpPresentationWhDto } from '../oid4vc-issuance/dtos/oid4vp-presentation-wh.dto'; + @Controller() @UseFilters(CustomExceptionFilter) @ApiTags('OID4VP') diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index 6ed8bdeae..3642cf478 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -138,3 +138,9 @@ export const getAgentUrl = (agentEndPoint: string, urlFlag: string, paramId?: st const url = `${agentEndPoint}${resolvedUrlPath}`; return url; }; + +export function shouldLoadOidcModules(): boolean { + const raw = process.env.HIDE_EXPERIMENTAL_OIDC_CONTROLLERS ?? 'true'; + const hide = 'true' === raw.toLowerCase(); + return !hide; +} From 69adb3eb63f0d1cb3c85a562b4e03bc6a64f0d4b Mon Sep 17 00:00:00 2001 From: Krishna Waske Date: Fri, 14 Nov 2025 19:13:36 +0530 Subject: [PATCH 37/43] fix: nats call microservices (#1514) * fix: nats call from nats client for verification service Signed-off-by: Krishna Waske * fix: ledger nats fix in agent-service Signed-off-by: Krishna Waske * fix: remove unused variable Signed-off-by: Krishna Waske * fix: nats calls in connection service Signed-off-by: Krishna Waske * fix: webhook service unwanted imports Signed-off-by: Krishna Waske * chore: add todos to remove nested mapping Signed-off-by: Krishna Waske * fix: sonar cube issues Signed-off-by: Krishna Waske * fix: coderabbit suggestions Signed-off-by: Krishna Waske --------- Signed-off-by: Krishna Waske --- .../src/agent-service.service.ts | 90 ++++----- .../src/interface/agent-service.interface.ts | 7 +- .../src/connection/connection.service.ts | 2 +- apps/cloud-wallet/src/cloud-wallet.service.ts | 9 +- apps/connection/src/connection.controller.ts | 18 +- apps/connection/src/connection.service.ts | 185 ++++++------------ apps/ledger/src/schema/schema.service.ts | 3 + .../src/interfaces/verification.interface.ts | 5 + .../src/verification.controller.ts | 6 +- apps/verification/src/verification.service.ts | 152 ++++++-------- apps/webhook/src/webhook.service.ts | 17 +- 11 files changed, 184 insertions(+), 310 deletions(-) diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index 5d3123d3a..648e64b9e 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -47,7 +47,6 @@ import { IDidCreate, IWallet, ITenantRecord, - LedgerListResponse, ICreateConnectionInvitation, IStoreAgent, AgentHealthData, @@ -55,7 +54,9 @@ import { IAgentConfigure, OrgDid, IBasicMessage, - WalletDetails + WalletDetails, + ILedger, + IStoreOrgAgent } from './interface/agent-service.interface'; import { AgentSpinUpStatus, AgentType, DidMethod, Ledgers, OrgAgentType, PromiseResult } from '@credebl/enum/enum'; import { AgentServiceRepository } from './repositories/agent-service.repository'; @@ -497,8 +498,15 @@ export class AgentServiceService { if (agentSpinupDto.method !== DidMethod.KEY && agentSpinupDto.method !== DidMethod.WEB) { const { network } = agentSpinupDto; const ledger = await ledgerName(network); - const ledgerList = (await this._getALlLedgerDetails()) as unknown as LedgerListResponse; - const isLedgerExist = ledgerList.response.find((existingLedgers) => existingLedgers.name === ledger); + const ledgerList = await this._getALlLedgerDetails(); + if (!ledgerList) { + throw new BadRequestException(ResponseMessages.agent.error.invalidLedger, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } + + const isLedgerExist = ledgerList.find((existingLedgers) => existingLedgers.name === ledger); if (!isLedgerExist) { throw new BadRequestException(ResponseMessages.agent.error.invalidLedger, { cause: new Error(), @@ -511,15 +519,14 @@ export class AgentServiceService { /** * Invoke wallet create and provision with agent */ - const walletProvision = await this._walletProvision(walletProvisionPayload); - if (!walletProvision?.response) { + const agentDetails = await this._walletProvision(walletProvisionPayload); + if (!agentDetails) { this.logger.error(`Agent not able to spin-up`); throw new BadRequestException(ResponseMessages.agent.error.notAbleToSpinup, { cause: new Error(), description: ResponseMessages.errorMessages.badRequest }); } - const agentDetails = walletProvision.response; const agentEndPoint = `${process.env.API_GATEWAY_PROTOCOL}://${agentDetails.agentEndPoint}`; /** * Socket connection @@ -693,48 +700,43 @@ export class AgentServiceService { } } - async _createConnectionInvitation( - orgId: string, - user: IUserRequestInterface, - label: string - ): Promise<{ - response; - }> { + async _createConnectionInvitation(orgId: string, user: IUserRequestInterface, label: string): Promise { try { const pattern = { cmd: 'create-connection-invitation' }; const payload = { createOutOfBandConnectionInvitation: { orgId, user, label } }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.agentServiceProxy, pattern, payload); + return result; } catch (error) { - this.logger.error(`error in create-connection in wallet provision : ${JSON.stringify(error)}`); + this.logger.error(`[natsCall] - error in create-connection in wallet provision : ${JSON.stringify(error)}`); + throw new RpcException(error?.response ? error.response : error); } } - async _getALlLedgerDetails(): Promise<{ - response; - }> { + async _getALlLedgerDetails(): Promise { try { const pattern = { cmd: 'get-all-ledgers' }; const payload = {}; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.agentServiceProxy, pattern, payload); + return result; } catch (error) { - this.logger.error(`error in while fetching all the ledger details : ${JSON.stringify(error)}`); + this.logger.error(`[natsCall] - error in while fetching all the ledger details : ${JSON.stringify(error)}`); + throw new RpcException(error?.response ? error.response : error); } } - async _walletProvision(payload: IWalletProvision): Promise<{ - response; - }> { + async _walletProvision(payload: IWalletProvision): Promise> { try { const pattern = { cmd: 'wallet-provisioning' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send>(this.agentServiceProxy, pattern, payload); + return result; } catch (error) { - this.logger.error(`error in wallet provision : ${JSON.stringify(error)}`); + this.logger.error(`[natsCall] - error in wallet provision : ${JSON.stringify(error)}`); throw error; } } @@ -795,8 +797,14 @@ export class AgentServiceService { ledger = Ledgers.Not_Applicable; } - const ledgerList = (await this._getALlLedgerDetails()) as unknown as LedgerListResponse; - const isLedgerExist = ledgerList.response.find((existingLedgers) => existingLedgers.name === ledger); + const ledgerList = await this._getALlLedgerDetails(); + if (!ledgerList) { + throw new BadRequestException(ResponseMessages.agent.error.invalidLedger, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } + const isLedgerExist = ledgerList.find((existingLedgers) => existingLedgers.name === ledger); if (!isLedgerExist) { throw new BadRequestException(ResponseMessages.agent.error.invalidLedger, { cause: new Error(), @@ -2191,32 +2199,6 @@ export class AgentServiceService { } } - async natsCall( - pattern: object, - payload: object - ): Promise<{ - response: string; - }> { - try { - return from(this.natsClient.send(this.agentServiceProxy, pattern, payload)) - .pipe(map((response) => ({ response }))) - .toPromise() - .catch((error) => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.message - }, - error.error - ); - }); - } catch (error) { - this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); - throw error; - } - } - private async tokenEncryption(token: string): Promise { try { const encryptedToken = CryptoJS.AES.encrypt(JSON.stringify(token), process.env.CRYPTO_PRIVATE_KEY).toString(); diff --git a/apps/agent-service/src/interface/agent-service.interface.ts b/apps/agent-service/src/interface/agent-service.interface.ts index 28b711b93..8c4a60197 100644 --- a/apps/agent-service/src/interface/agent-service.interface.ts +++ b/apps/agent-service/src/interface/agent-service.interface.ts @@ -213,6 +213,7 @@ export interface IStoreOrgAgent { id?: string; clientSocketId?: string; agentEndPoint?: string; + agentToken?: string; apiKey?: string; seed?: string; did?: string; @@ -556,7 +557,7 @@ export interface IQuestionPayload { export interface IBasicMessage { content: string; } -interface Ledger { +export interface ILedger { id: string; createDateTime: string; lastChangedDateTime: string; @@ -570,10 +571,6 @@ interface Ledger { networkUrl: string | null; } -export interface LedgerListResponse { - response: Ledger[]; -} - export interface ICreateConnectionInvitation { label?: string; alias?: string; diff --git a/apps/api-gateway/src/connection/connection.service.ts b/apps/api-gateway/src/connection/connection.service.ts index 88efdbad7..d08d181ca 100644 --- a/apps/api-gateway/src/connection/connection.service.ts +++ b/apps/api-gateway/src/connection/connection.service.ts @@ -32,7 +32,7 @@ export class ConnectionService extends BaseService { try { return this.natsClient.sendNatsMessage(this.connectionServiceProxy, 'send-question', questionDto); } catch (error) { - throw new RpcException(error.response); + throw new RpcException(error?.response ?? error); } } diff --git a/apps/cloud-wallet/src/cloud-wallet.service.ts b/apps/cloud-wallet/src/cloud-wallet.service.ts index 0a396251a..988ada3db 100644 --- a/apps/cloud-wallet/src/cloud-wallet.service.ts +++ b/apps/cloud-wallet/src/cloud-wallet.service.ts @@ -3,14 +3,11 @@ import { CommonService } from '@credebl/common'; import { BadRequestException, ConflictException, - Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; -import { Cache } from 'cache-manager'; -import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { IAcceptOffer, ICreateCloudWalletDid, @@ -40,17 +37,13 @@ import { CloudWalletRepository } from './cloud-wallet.repository'; import { ResponseMessages } from '@credebl/common/response-messages'; import { CloudWalletType } from '@credebl/enum/enum'; import { CommonConstants } from '@credebl/common/common.constant'; -import { ClientProxy } from '@nestjs/microservices'; @Injectable() export class CloudWalletService { constructor( private readonly commonService: CommonService, - @Inject('NATS_CLIENT') private readonly cloudWalletServiceProxy: ClientProxy, private readonly cloudWalletRepository: CloudWalletRepository, - private readonly logger: Logger, - // TODO: Remove duplicate, unused variable - @Inject(CACHE_MANAGER) private cacheService: Cache + private readonly logger: Logger ) {} /** diff --git a/apps/connection/src/connection.controller.ts b/apps/connection/src/connection.controller.ts index 0cc2e63d7..cb5c92fbd 100644 --- a/apps/connection/src/connection.controller.ts +++ b/apps/connection/src/connection.controller.ts @@ -46,15 +46,15 @@ export class ConnectionController { } @MessagePattern({ cmd: 'get-all-agent-connection-list' }) - async getConnectionListFromAgent(payload: GetAllConnections): Promise { - const {orgId, connectionSearchCriteria } = payload; + async getConnectionListFromAgent(payload: GetAllConnections): Promise { + const { orgId, connectionSearchCriteria } = payload; return this.connectionService.getAllConnectionListFromAgent(orgId, connectionSearchCriteria); } /** - * + * * @param connectionId - * @param orgId + * @param orgId * @returns connection details by connection Id */ @MessagePattern({ cmd: 'get-connection-details-by-connectionId' }) @@ -64,7 +64,7 @@ export class ConnectionController { } @MessagePattern({ cmd: 'get-connection-records' }) - async getConnectionRecordsByOrgId(payload: { orgId: string, userId: string }): Promise { + async getConnectionRecordsByOrgId(payload: { orgId: string; userId: string }): Promise { const { orgId } = payload; return this.connectionService.getConnectionRecords(orgId); } @@ -80,7 +80,7 @@ export class ConnectionController { const { user, receiveInvitation, orgId } = payload; return this.connectionService.receiveInvitation(user, receiveInvitation, orgId); } - + @MessagePattern({ cmd: 'send-question' }) async sendQuestion(payload: IQuestionPayload): Promise { return this.connectionService.sendQuestion(payload); @@ -97,13 +97,13 @@ export class ConnectionController { } @MessagePattern({ cmd: 'delete-connection-records' }) - async deleteConnectionRecords(payload: {orgId: string, userDetails: user}): Promise { + async deleteConnectionRecords(payload: { orgId: string; userDetails: user }): Promise { const { orgId, userDetails } = payload; return this.connectionService.deleteConnectionRecords(orgId, userDetails); } @MessagePattern({ cmd: 'send-basic-message-on-connection' }) - async sendBasicMessage(payload: {content: string, orgId: string, connectionId: string}): Promise { - return this.connectionService.sendBasicMesage(payload); + async sendBasicMessage(payload: { content: string; orgId: string; connectionId: string }): Promise { + return this.connectionService.sendBasicMessage(payload); } } diff --git a/apps/connection/src/connection.service.ts b/apps/connection/src/connection.service.ts index 8442c7421..0d45f2ae1 100644 --- a/apps/connection/src/connection.service.ts +++ b/apps/connection/src/connection.service.ts @@ -3,7 +3,6 @@ import { CommonService } from '@credebl/common'; import { CommonConstants } from '@credebl/common/common.constant'; import { HttpException, HttpStatus, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { ClientProxy, RpcException } from '@nestjs/microservices'; -import { from, map } from 'rxjs'; import { ConnectionResponseDetail, AgentConnectionSearchCriteria, @@ -19,8 +18,6 @@ import { ConnectionRepository } from './connection.repository'; import { ResponseMessages } from '@credebl/common/response-messages'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { ConnectionProcessState } from '@credebl/enum/enum'; -import { Cache } from 'cache-manager'; -import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { IConnectionList, ICreateConnectionUrl, @@ -41,8 +38,6 @@ export class ConnectionService { private readonly connectionRepository: ConnectionRepository, private readonly userActivityRepository: UserActivityRepository, private readonly logger: Logger, - // TODO: Remove unused variable - @Inject(CACHE_MANAGER) private readonly cacheService: Cache, private readonly natsClient: NATSClient ) {} @@ -57,7 +52,7 @@ export class ConnectionService { return saveConnectionDetails; } catch (error) { this.logger.error(`[getConnectionWebhook] - error in fetch connection webhook: ${error}`); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } @@ -66,19 +61,14 @@ export class ConnectionService { * @param orgId * @returns connection invitation URL */ - async _createConnectionInvitation( - connectionPayload: object, - url: string, - orgId: string - ): Promise<{ - response; - }> { + async _createConnectionInvitation(connectionPayload: object, url: string, orgId: string): Promise { //nats call in agent-service to create an invitation url const pattern = { cmd: 'agent-create-connection-legacy-invitation' }; const payload = { connectionPayload, url, orgId }; try { - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.connectionServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( @@ -107,7 +97,7 @@ export class ConnectionService { this.logger.error( `[getConnectionRecords ] [NATS call]- error in get connection records count : ${JSON.stringify(error)}` ); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } @@ -122,7 +112,7 @@ export class ConnectionService { return urlDetails.referenceId; } catch (error) { this.logger.error(`Error in get url in connection service: ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } @@ -178,14 +168,14 @@ export class ConnectionService { } catch (error) { this.logger.error(`[getConnections] [NATS call]- error in fetch connections details : ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } async getAllConnectionListFromAgent( orgId: string, connectionSearchCriteria: AgentConnectionSearchCriteria - ): Promise { + ): Promise { try { const { alias, myDid, outOfBandId, state, theirDid, theirLabel } = connectionSearchCriteria; const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); @@ -223,26 +213,22 @@ export class ConnectionService { } const connectionResponse = await this._getAllConnections(url, orgId); - return connectionResponse.response; + return connectionResponse; } catch (error) { this.logger.error( `[getConnectionsFromAgent] [NATS call]- error in fetch connections details : ${JSON.stringify(error)}` ); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error); } } - async _getAllConnections( - url: string, - orgId: string - ): Promise<{ - response: string; - }> { + async _getAllConnections(url: string, orgId: string): Promise { try { const pattern = { cmd: 'agent-get-all-connections' }; const payload = { url, orgId }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.connectionServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error( `[_getAllConnections] [NATS call]- error in fetch connections details : ${JSON.stringify(error)}` @@ -260,18 +246,18 @@ export class ConnectionService { } const url = `${agentEndPoint}${CommonConstants.URL_CONN_GET_CONNECTION_BY_ID}`.replace('#', connectionId); const createConnectionInvitation = await this._getConnectionsByConnectionId(url, orgId); - return createConnectionInvitation?.response; + return createConnectionInvitation; } catch (error) { this.logger.error(`[getConnectionsById] - error in get connections : ${JSON.stringify(error)}`); - if (error?.response?.error?.reason) { + if (error?.error?.reason) { throw new RpcException({ message: ResponseMessages.connection.error.connectionNotFound, - statusCode: error?.response?.status, - error: error?.response?.error?.reason + statusCode: error?.status, + error: error?.error?.reason }); } else { - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error); } } } @@ -293,41 +279,24 @@ export class ConnectionService { } } - async _getConnectionsByConnectionId( - url: string, - orgId: string - ): Promise<{ - response; - }> { + async _getConnectionsByConnectionId(url: string, orgId: string): Promise { //nats call in agent service for fetch connection details const pattern = { cmd: 'agent-get-connection-details-by-connectionId' }; const payload = { url, orgId }; - return this.natsCall(pattern, payload); + return this.natsClient.send(this.connectionServiceProxy, pattern, payload); } async _getQuestionAnswersRecord(url: string, orgId: string): Promise { - const pattern = { cmd: 'agent-get-question-answer-record' }; - const payload = { url, orgId }; - return this.natsCall(pattern, payload); - } - - async _getOrgAgentApiKey(orgId: string): Promise<{ - response: string; - }> { - const pattern = { cmd: 'get-org-agent-api-key' }; - const payload = { orgId }; - try { - return await this.natsCall(pattern, payload); + const pattern = { cmd: 'agent-get-question-answer-record' }; + const payload = { url, orgId }; + const result = await this.natsClient.send(this.connectionServiceProxy, pattern, payload); + return result; } catch (error) { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.status, - error: error.message - }, - error.status + this.logger.error( + `[_getQuestionAnswersRecord ] [NATS call]- error in get question and answer records : ${JSON.stringify(error)}` ); + throw error; } } @@ -344,7 +313,7 @@ export class ConnectionService { } const url = `${agentEndPoint}${CommonConstants.URL_RECEIVE_INVITATION_URL}`; const createConnectionInvitation = await this._receiveInvitationUrl(url, orgId, receiveInvitationUrl); - return createConnectionInvitation.response; + return createConnectionInvitation; } catch (error) { this.logger.error(`[receiveInvitationUrl] - error in receive invitation url : ${JSON.stringify(error, null, 2)}`); @@ -355,14 +324,14 @@ export class ConnectionService { message: customErrorMessage, error: ResponseMessages.errorMessages.conflict }); - } else if (error?.response?.error?.reason) { + } else if (error?.error?.reason) { throw new RpcException({ message: ResponseMessages.connection.error.connectionNotFound, - statusCode: error?.response?.status, - error: error?.response?.error?.reason + statusCode: error?.status, + error: error?.error?.reason }); } else { - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } } @@ -371,14 +340,17 @@ export class ConnectionService { url: string, orgId: string, receiveInvitationUrl: IReceiveInvitationUrl - ): Promise<{ - response; - }> { + ): Promise { const pattern = { cmd: 'agent-receive-invitation-url' }; const payload = { url, orgId, receiveInvitationUrl }; try { - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send( + this.connectionServiceProxy, + pattern, + payload + ); + return result; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( @@ -404,18 +376,18 @@ export class ConnectionService { } const url = `${agentEndPoint}${CommonConstants.URL_RECEIVE_INVITATION}`; const createConnectionInvitation = await this._receiveInvitation(url, orgId, receiveInvitation); - return createConnectionInvitation?.response; + return createConnectionInvitation; } catch (error) { this.logger.error(`[receiveInvitation] - error in receive invitation : ${JSON.stringify(error)}`); - if (error?.response?.error?.reason) { + if (error?.error?.reason) { throw new RpcException({ message: ResponseMessages.connection.error.connectionNotFound, - statusCode: error?.response?.status, - error: error?.response?.error?.reason + statusCode: error?.status, + error: error?.error?.reason }); } else { - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } } @@ -424,18 +396,16 @@ export class ConnectionService { url: string, orgId: string, receiveInvitation: IReceiveInvitation - ): Promise<{ - response; - }> { + ): Promise { const pattern = { cmd: 'agent-receive-invitation' }; const payload = { url, orgId, receiveInvitation }; - return this.natsCall(pattern, payload); + return this.natsClient.send(this.connectionServiceProxy, pattern, payload); } async _sendQuestion(questionPayload: IQuestionPayload, url: string, orgId: string): Promise { const pattern = { cmd: 'agent-send-question' }; const payload = { questionPayload, url, orgId }; - return this.natsCall(pattern, payload); + return this.natsClient.send(this.connectionServiceProxy, pattern, payload); } async sendQuestion(payload: IQuestionPayload): Promise { @@ -467,7 +437,7 @@ export class ConnectionService { statusCode: error.status?.code ?? HttpStatus.INTERNAL_SERVER_ERROR }); } - throw new RpcException(error.response || error); + throw new RpcException(error?.response ?? error); } } @@ -478,8 +448,8 @@ export class ConnectionService { const payload = { persistent, storeObj }; try { - const message = await this.natsCall(pattern, payload); - return message.response; + const message = await this.natsClient.send(this.connectionServiceProxy, pattern, payload); + return message; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( @@ -561,13 +531,13 @@ export class ConnectionService { }; const url = await getAgentUrl(agentEndPoint, CommonConstants.CONNECTION_INVITATION); const createConnectionInvitation = await this._createOutOfBandConnectionInvitation(connectionPayload, url, orgId); - const connectionInvitationUrl = createConnectionInvitation?.response?.invitationUrl; + const connectionInvitationUrl = createConnectionInvitation?.invitationUrl; const shortenedUrl = await this.storeConnectionObjectAndReturnUrl( connectionInvitationUrl, connectionPayload.multiUseInvitation ); - const invitationsDid = createConnectionInvitation?.response?.invitationDid || invitationDid; + const invitationsDid = createConnectionInvitation?.invitationDid || invitationDid; const saveConnectionDetails = await this.connectionRepository.saveAgentConnectionInvitations( shortenedUrl, agentId, @@ -584,7 +554,7 @@ export class ConnectionService { createdBy: saveConnectionDetails.createdBy, lastChangedDateTime: saveConnectionDetails.lastChangedDateTime, lastChangedBy: saveConnectionDetails.lastChangedBy, - recordId: createConnectionInvitation.response.outOfBandRecord.id, + recordId: createConnectionInvitation.outOfBandRecord.id, invitationDid: saveConnectionDetails.invitationDid }; return connectionStorePayload; @@ -603,15 +573,15 @@ export class ConnectionService { connectionPayload: ICreateConnectionInvitation, url: string, orgId: string - ): Promise<{ - response; - }> { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise { //nats call in agent-service to create an invitation url const pattern = { cmd: 'agent-create-connection-invitation' }; const payload = { connectionPayload, url, orgId }; try { - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.connectionServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( @@ -624,36 +594,6 @@ export class ConnectionService { } } - async natsCall( - pattern: object, - payload: object - ): Promise<{ - response: string; - }> { - try { - return from(this.natsClient.send(this.connectionServiceProxy, pattern, payload)) - .pipe( - map((response) => ({ - response - })) - ) - .toPromise() - .catch((error) => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.message - }, - error.error - ); - }); - } catch (error) { - this.logger.error(`[ConnectionService natsCall] - error in nats call : ${JSON.stringify(error)}`); - throw error; - } - } - handleError(error): Promise { if (error?.status?.message?.error) { throw new RpcException({ @@ -661,7 +601,7 @@ export class ConnectionService { statusCode: error.status?.code ?? HttpStatus.INTERNAL_SERVER_ERROR }); } - throw new RpcException(error.response || error); + throw new RpcException(error?.response ?? error); } async deleteConnectionRecords(orgId: string, user: user): Promise { @@ -703,11 +643,11 @@ export class ConnectionService { return deleteConnections; } catch (error) { this.logger.error(`[deleteConnectionRecords] - error in deleting connection records: ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error?.response ?? error); } } - async sendBasicMesage(payload: IBasicMessage): Promise { + async sendBasicMessage(payload: IBasicMessage): Promise { const { content, orgId, connectionId } = payload; try { const agentDetails = await this.connectionRepository.getAgentEndPoint(orgId); @@ -733,14 +673,13 @@ export class ConnectionService { statusCode: error.status?.code ?? HttpStatus.INTERNAL_SERVER_ERROR }); } - throw new RpcException(error.response || error); + throw new RpcException(error?.response ?? error); } } async _sendBasicMessageToAgent(content: IBasicMessage, url: string, orgId: string): Promise { const pattern = { cmd: 'agent-send-basic-message' }; const payload = { content, url, orgId }; - // eslint-disable-next-line no-return-await - return await this.natsCall(pattern, payload); + return this.natsClient.send(this.connectionServiceProxy, pattern, payload); } } diff --git a/apps/ledger/src/schema/schema.service.ts b/apps/ledger/src/schema/schema.service.ts index 276c840ed..d4b4f5f31 100644 --- a/apps/ledger/src/schema/schema.service.ts +++ b/apps/ledger/src/schema/schema.service.ts @@ -402,6 +402,7 @@ export class SchemaService extends BaseService { }; const schemaResponse = await from(this.natsClient.send(this.schemaServiceProxy, pattern, payload)) .pipe( + // TODO: remove nested mapping map((response) => ({ response })) @@ -429,6 +430,7 @@ export class SchemaService extends BaseService { }; const W3CSchemaResponse = await from(this.natsClient.send(this.schemaServiceProxy, natsPattern, payload)) .pipe( + // TODO: remove nested mapping map((response) => ({ response })) @@ -553,6 +555,7 @@ export class SchemaService extends BaseService { }; const schemaResponse = await from(this.natsClient.send(this.schemaServiceProxy, pattern, payload)) .pipe( + // TODO: remove nested mapping map((response) => ({ response })) diff --git a/apps/verification/src/interfaces/verification.interface.ts b/apps/verification/src/interfaces/verification.interface.ts index 2e259a785..909b72c06 100644 --- a/apps/verification/src/interfaces/verification.interface.ts +++ b/apps/verification/src/interfaces/verification.interface.ts @@ -236,6 +236,11 @@ export interface IProofRequestSearchCriteria { } export interface IInvitation { + outOfBandRecord?: { + id: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } & Record; + proofRecordThId?: string; invitationUrl?: string; deepLinkURL?: string; } diff --git a/apps/verification/src/verification.controller.ts b/apps/verification/src/verification.controller.ts index 5b795d419..907c00a77 100644 --- a/apps/verification/src/verification.controller.ts +++ b/apps/verification/src/verification.controller.ts @@ -45,7 +45,7 @@ export class VerificationController { * @returns Proof presentation details by proofId */ @MessagePattern({ cmd: 'get-proof-presentations-by-proofId' }) - async getProofPresentationById(payload: { proofId: string; orgId: string; user: IUserRequest }): Promise { + async getProofPresentationById(payload: { proofId: string; orgId: string; user: IUserRequest }): Promise { return this.verificationService.getProofPresentationById(payload.proofId, payload.orgId); } @@ -68,7 +68,7 @@ export class VerificationController { async sendProofRequest(payload: { requestProofDto: IProofRequestData; user: IUserRequest; - }): Promise { + }): Promise { return this.verificationService.sendProofRequest(payload.requestProofDto); } @@ -79,7 +79,7 @@ export class VerificationController { * @returns Verified proof presentation details */ @MessagePattern({ cmd: 'verify-presentation' }) - async verifyPresentation(payload: { proofId: string; orgId: string; user: IUserRequest }): Promise { + async verifyPresentation(payload: { proofId: string; orgId: string; user: IUserRequest }): Promise { return this.verificationService.verifyPresentation(payload.proofId, payload.orgId); } diff --git a/apps/verification/src/verification.service.ts b/apps/verification/src/verification.service.ts index 1a4cb77d1..09013b019 100644 --- a/apps/verification/src/verification.service.ts +++ b/apps/verification/src/verification.service.ts @@ -9,7 +9,6 @@ import { NotFoundException } from '@nestjs/common'; import { ClientProxy, RpcException } from '@nestjs/microservices'; -import { map } from 'rxjs/operators'; import { IGetAllProofPresentations, IProofRequestSearchCriteria, @@ -52,7 +51,6 @@ import { convertUrlToDeepLinkUrl, getAgentUrl } from '@credebl/common/common.uti import { UserActivityRepository } from 'libs/user-activity/repositories'; import { ISchemaDetail } from '@credebl/common/interfaces/schema.interface'; import { NATSClient } from '@credebl/common/NATSClient'; -import { from } from 'rxjs'; import { EmailService } from '@credebl/common/email.service'; @Injectable() @@ -184,12 +182,11 @@ export class VerificationService { * @param payload * @returns Get all proof presentation */ - async _getProofPresentations(payload: IGetAllProofPresentations): Promise<{ - response: string; - }> { + async _getProofPresentations(payload: IGetAllProofPresentations): Promise { try { const pattern = { cmd: 'agent-get-proof-presentations' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`[_getProofPresentations] - error in get proof presentations : ${JSON.stringify(error)}`); throw error; @@ -225,7 +222,7 @@ export class VerificationService { * @param orgId * @returns Proof presentation details by proofId */ - async getProofPresentationById(proofId: string, orgId: string): Promise { + async getProofPresentationById(proofId: string, orgId: string): Promise { try { const getAgentDetails = await this.verificationRepository.getAgentEndPoint(orgId); const url = await getAgentUrl( @@ -237,18 +234,18 @@ export class VerificationService { const payload = { orgId, url }; const getProofPresentationById = await this._getProofPresentationById(payload); - return getProofPresentationById?.response; + return getProofPresentationById; } catch (error) { this.logger.error( `[getProofPresentationById] - error in get proof presentation by proofId : ${JSON.stringify(error)}` ); - const errorMessage = error?.response?.error?.reason || error?.message; + const errorMessage = error?.error?.reason || error?.message; if (errorMessage?.includes('not found')) { throw new NotFoundException(errorMessage); } - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error); } } @@ -257,15 +254,14 @@ export class VerificationService { * @param payload * @returns Get proof presentation details */ - async _getProofPresentationById(payload: IGetProofPresentationById): Promise<{ - response: string; - }> { + async _getProofPresentationById(payload: IGetProofPresentationById): Promise { try { const pattern = { cmd: 'agent-get-proof-presentation-by-id' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error( `[_getProofPresentationById] - error in get proof presentation by id : ${JSON.stringify(error)}` @@ -280,7 +276,7 @@ export class VerificationService { * @returns Requested proof presentation details */ - async sendProofRequest(requestProof: IProofRequestData): Promise { + async sendProofRequest(requestProof: IProofRequestData): Promise { try { const comment = requestProof.comment ? requestProof.comment : ''; const getAgentDetails = await this.verificationRepository.getAgentEndPoint(requestProof.orgId); @@ -338,11 +334,11 @@ export class VerificationService { ? requestProof.connectionId : [requestProof.connectionId]; - const responses: string[] = []; + const responses: object[] = []; for (const connectionId of connectionIds) { const payload = await createPayload(connectionId); const getProofPresentationById = await this._sendProofRequest(payload); - responses.push(getProofPresentationById?.response); + responses.push(getProofPresentationById); } return responses; } else { @@ -352,7 +348,7 @@ export class VerificationService { const payload = await createPayload(connectionId); const getProofPresentationById = await this._sendProofRequest(payload); - return getProofPresentationById?.response; + return getProofPresentationById; } } catch (error) { // Handle cases where identical attributes are used in both predicates and non-predicates. @@ -381,17 +377,16 @@ export class VerificationService { * @param orgId * @returns Get requested proof presentation details */ - async _sendProofRequest(payload: IProofRequestPayload): Promise<{ - response: string; - }> { + async _sendProofRequest(payload: IProofRequestPayload): Promise { try { const pattern = { cmd: 'agent-send-proof-request' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { - this.logger.error(`[_sendProofRequest] - error in verify presentation : ${JSON.stringify(error)}`); + this.logger.error(`[_sendProofRequest] - nats error in sending proof request : ${JSON.stringify(error)}`); throw error; } } @@ -402,28 +397,28 @@ export class VerificationService { * @param orgId * @returns Verified proof presentation details */ - async verifyPresentation(proofId: string, orgId: string): Promise { + async verifyPresentation(proofId: string, orgId: string): Promise { try { const getAgentData = await this.verificationRepository.getAgentEndPoint(orgId); const url = await getAgentUrl(getAgentData?.agentEndPoint, CommonConstants.ACCEPT_PRESENTATION, proofId); const payload = { orgId, url }; const getProofPresentationById = await this._verifyPresentation(payload); - return getProofPresentationById?.response; + return getProofPresentationById; } catch (error) { this.logger.error( `[getProofPresentationById] - error in get proof presentation by proofId : ${JSON.stringify(error)}` ); - const errorStack = error?.response?.error?.reason; + const errorStack = error?.error?.reason; if (errorStack) { throw new RpcException({ message: ResponseMessages.verification.error.proofNotFound, - statusCode: error?.response?.status, + statusCode: error?.status, error: errorStack }); } else { - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error || 'Something went wrong'); } } } @@ -433,15 +428,14 @@ export class VerificationService { * @param payload * @returns Get verified proof presentation details */ - async _verifyPresentation(payload: IVerifyPresentation): Promise<{ - response: string; - }> { + async _verifyPresentation(payload: IVerifyPresentation): Promise { try { const pattern = { cmd: 'agent-verify-presentation' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`[_verifyPresentation] - error in verify presentation : ${JSON.stringify(error)}`); throw error; @@ -571,17 +565,17 @@ export class VerificationService { //nats call in agent-service to create an invitation url const pattern = { cmd: 'store-object-return-url' }; const payload = { persistent, storeObj }; - const message = await this.natsCall(pattern, payload); - return message.response; + const message = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return message; } - private async generateOOBProofReq(payload: IProofRequestPayload): Promise { + private async generateOOBProofReq(payload: IProofRequestPayload): Promise { const getProofPresentation = await this._sendOutOfBandProofRequest(payload); if (!getProofPresentation) { throw new Error(ResponseMessages.verification.error.proofPresentationNotFound); } - return getProofPresentation.response; + return getProofPresentation; } // Currently batch size is not used, as length of emails sent is restricted to '10' @@ -626,13 +620,13 @@ export class VerificationService { getAgentDetails: org_agents, organizationDetails: organisation ): Promise { - const getProofPresentation = await this._sendOutOfBandProofRequest(payload); + const getProofPresentation: IInvitation = await this._sendOutOfBandProofRequest(payload); if (!getProofPresentation) { throw new Error(ResponseMessages.verification.error.proofPresentationNotFound); } - const invitationUrl = getProofPresentation?.response?.invitationUrl; + const invitationUrl = getProofPresentation?.invitationUrl; // Currently have shortenedUrl to store only for 30 days const persist: boolean = false; const shortenedUrl = await this.storeVerificationObjectAndReturnUrl(invitationUrl, persist); @@ -670,8 +664,8 @@ export class VerificationService { return { isEmailSent, - outOfBandRecordId: getProofPresentation?.response?.outOfBandRecord?.id, - proofRecordThId: getProofPresentation?.response?.proofRecordThId + outOfBandRecordId: getProofPresentation?.outOfBandRecord?.id, + proofRecordThId: getProofPresentation?.proofRecordThId }; } @@ -680,15 +674,14 @@ export class VerificationService { * @param payload * @returns Get requested proof presentation details */ - async _sendOutOfBandProofRequest(payload: IProofRequestPayload): Promise<{ - response; - }> { + async _sendOutOfBandProofRequest(payload: IProofRequestPayload): Promise { try { const pattern = { cmd: 'agent-send-out-of-band-proof-request' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`[_sendOutOfBandProofRequest] - error in Out Of Band Presentation : ${JSON.stringify(error)}`); throw error; @@ -771,7 +764,7 @@ export class VerificationService { const getProofPresentationById = await this._getVerifiedProofDetails(payload); - if (!getProofPresentationById?.response?.presentation) { + if (!getProofPresentationById?.presentation) { throw new NotFoundException(ResponseMessages.verification.error.proofPresentationNotFound, { cause: new Error(), description: ResponseMessages.errorMessages.notFound @@ -781,11 +774,10 @@ export class VerificationService { const extractedDataArray: IProofPresentationDetails[] = []; // For Presentation Exchange format - if (getProofPresentationById?.response?.request?.presentationExchange) { - const presentationDefinition = - getProofPresentationById?.response?.request?.presentationExchange?.presentation_definition; + if (getProofPresentationById?.request?.presentationExchange) { + const presentationDefinition = getProofPresentationById?.request?.presentationExchange?.presentation_definition; const verifiableCredentials = - getProofPresentationById?.response?.presentation?.presentationExchange?.verifiableCredential; + getProofPresentationById?.presentation?.presentationExchange?.verifiableCredential; presentationDefinition?.input_descriptors.forEach((descriptor, index) => { const schemaId = descriptor?.schema[0]?.uri; @@ -795,7 +787,7 @@ export class VerificationService { if (getProofPresentationById?.response) { certificate = - getProofPresentationById?.response?.presentation?.presentationExchange?.verifiableCredential[0].prettyVc + getProofPresentationById?.presentation?.presentationExchange?.verifiableCredential[0].prettyVc ?.certificate; } @@ -822,10 +814,10 @@ export class VerificationService { }); } // For Indy format - if (getProofPresentationById?.response?.request?.indy) { - const requestedAttributes = getProofPresentationById?.response?.request?.indy?.requested_attributes; - const requestedPredicates = getProofPresentationById?.response?.request?.indy?.requested_predicates; - const revealedAttrs = getProofPresentationById?.response?.presentation?.indy?.requested_proof?.revealed_attrs; + if (getProofPresentationById?.request?.indy) { + const requestedAttributes = getProofPresentationById?.request?.indy?.requested_attributes; + const requestedPredicates = getProofPresentationById?.request?.indy?.requested_predicates; + const revealedAttrs = getProofPresentationById?.presentation?.indy?.requested_proof?.revealed_attrs; if (0 !== Object.keys(requestedAttributes).length && 0 !== Object.keys(requestedPredicates).length) { for (const key in requestedAttributes) { @@ -836,9 +828,9 @@ export class VerificationService { if (requestedAttributeKey?.restrictions) { credDefId = requestedAttributeKey?.restrictions[0]?.cred_def_id; schemaId = requestedAttributeKey?.restrictions[0]?.schema_id; - } else if (getProofPresentationById?.response?.presentation?.indy?.identifiers) { - credDefId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].cred_def_id; - schemaId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].schema_id; + } else if (getProofPresentationById?.presentation?.indy?.identifiers) { + credDefId = getProofPresentationById?.presentation?.indy?.identifiers[0].cred_def_id; + schemaId = getProofPresentationById?.presentation?.indy?.identifiers[0].schema_id; } if (revealedAttrs.hasOwnProperty(key)) { @@ -915,16 +907,16 @@ export class VerificationService { return extractedDataArray; } catch (error) { this.logger.error(`[getVerifiedProofDetails] - error in get verified proof details : ${JSON.stringify(error)}`); - const errorStack = error?.response?.error?.reason; + const errorStack = error?.error?.reason; if (errorStack) { throw new RpcException({ message: ResponseMessages.verification.error.verifiedProofNotFound, - statusCode: error?.response?.status, + statusCode: error?.status, error: errorStack }); } else { - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error); } } } @@ -936,24 +928,24 @@ export class VerificationService { if (attribute?.restrictions) { credDefId = attribute?.restrictions[0]?.cred_def_id; schemaId = attribute?.restrictions[0]?.schema_id; - } else if (getProofPresentationById?.response?.presentation?.indy?.identifiers) { - credDefId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].cred_def_id; - schemaId = getProofPresentationById?.response?.presentation?.indy?.identifiers[0].schema_id; + } else if (getProofPresentationById?.presentation?.indy?.identifiers) { + credDefId = getProofPresentationById?.presentation?.indy?.identifiers[0].cred_def_id; + schemaId = getProofPresentationById?.presentation?.indy?.identifiers[0].schema_id; } return [credDefId, schemaId]; } - async _getVerifiedProofDetails(payload: IVerifiedProofData): Promise<{ - response; - }> { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async _getVerifiedProofDetails(payload: IVerifiedProofData): Promise { try { //nats call in agent for fetch verified proof details const pattern = { cmd: 'get-agent-verified-proof-details' }; - return await this.natsCall(pattern, payload); + const result = await this.natsClient.send(this.verificationServiceProxy, pattern, payload); + return result; } catch (error) { this.logger.error(`[_getVerifiedProofDetails] - error in verified proof details : ${JSON.stringify(error)}`); throw error; @@ -993,32 +985,6 @@ export class VerificationService { } } - async natsCall( - pattern: object, - payload: object - ): Promise<{ - response: string; - }> { - return from(this.natsClient.send(this.verificationServiceProxy, pattern, payload)) - .pipe( - map((response) => ({ - response - })) - ) - .toPromise() - .catch((error) => { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.error, - message: error.message - }, - error.error - ); - }); - } - async delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/apps/webhook/src/webhook.service.ts b/apps/webhook/src/webhook.service.ts index 3d3402bd2..f0e899261 100644 --- a/apps/webhook/src/webhook.service.ts +++ b/apps/webhook/src/webhook.service.ts @@ -1,27 +1,16 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { CommonService } from '@credebl/common'; import { WebhookRepository } from './webhook.repository'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { RpcException } from '@nestjs/microservices'; import AsyncRetry = require('async-retry'); import { ICreateWebhookUrl, IGetWebhookUrl, IWebhookDto } from '../interfaces/webhook.interfaces'; -import { - Inject, - Injectable, - InternalServerErrorException, - Logger, - NotFoundException -} from '@nestjs/common'; +import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; import { IWebhookUrl } from '@credebl/common/interfaces/webhook.interface'; @Injectable() export class WebhookService { private readonly logger = new Logger('WebhookService'); - constructor( - @Inject('NATS_CLIENT') private readonly webhookProxy: ClientProxy, - private readonly commonService: CommonService, - private readonly webhookRepository: WebhookRepository - ) {} + constructor(private readonly webhookRepository: WebhookRepository) {} // eslint-disable-next-line @typescript-eslint/explicit-function-return-type retryOptions(logger: Logger) { From 9fe448646d7c86dca12b1daeb0b92874453ad3c1 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Tue, 18 Nov 2025 11:32:12 +0530 Subject: [PATCH 38/43] fixed coderabbit comments for PR:1517 Signed-off-by: Rinkal Bhojani --- .env.sample | 30 ++++----- .../agent-service/dto/create-schema.dto.ts | 10 ++- .../src/dtos/issue-credential-offer.dto .ts | 64 ------------------- .../src/helper-files/file-operation.helper.ts | 18 +++--- .../src/issuance/issuance.service.ts | 3 - libs/context/src/contextInterceptorModule.ts | 5 +- libs/context/src/contextModule.ts | 1 + libs/logger/src/logging.interceptor.ts | 26 ++++---- 8 files changed, 46 insertions(+), 111 deletions(-) delete mode 100644 apps/api-gateway/src/dtos/issue-credential-offer.dto .ts diff --git a/.env.sample b/.env.sample index 0ee053975..2300a7237 100644 --- a/.env.sample +++ b/.env.sample @@ -110,21 +110,21 @@ AGENT_PROTOCOL=http GEO_LOCATION_MASTER_DATA_IMPORT_SCRIPT=/prisma/scripts/geo_location_data_import.sh UPDATE_CLIENT_CREDENTIAL_SCRIPT=/prisma/scripts/update_client_credential_data.sh -USER_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for user service -API_GATEWAY_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for api-gateway -ORGANIZATION_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for organization service -AGENT_PROVISIONING_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for agent provisioning service -AGENT_SERVICE_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for agent service -VERIFICATION_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for verification service -ISSUANCE_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for issuance service -CONNECTION_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for connection service -CREDENTAILDEFINITION_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for credential-definition service -SCHEMA_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for schema service -UTILITIES_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for utilities service -GEOLOCATION_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for geo-location service -X509_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for x509 service -OIDC4VC_ISSUANCE_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for x509 service -OIDC4VC_VERIFICATION_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for x509 service +USER_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for user service +API_GATEWAY_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for api-gateway +ORGANIZATION_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for organization service +AGENT_PROVISIONING_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for agent provisioning service +AGENT_SERVICE_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for agent service +VERIFICATION_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for verification service +ISSUANCE_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for issuance service +CONNECTION_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for connection service +CREDENTAILDEFINITION_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for credential-definition service +SCHEMA_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for schema service +UTILITIES_NKEY_SEED=xxxxxxxxxxxxx // Please provide Nkeys secret for utilities service +GEOLOCATION_NKEY_SEED=xxxxxxxxxxx // Please provide Nkeys secret for geo-location service +X509_NKEY_SEED=xxxxxxxxxxx // Please provide Nkeys secret for x509 service +OIDC4VC_ISSUANCE_NKEY_SEED=xxxxxxxxxxx // Please provide Nkeys secret for x509 service +OIDC4VC_VERIFICATION_NKEY_SEED=xxxxxxxxxxx // Please provide Nkeys secret for x509 service AFJ_AGENT_TOKEN_PATH=/apps/agent-provisioning/AFJ/token/ diff --git a/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts index e8a8a1a0c..5cc00c285 100644 --- a/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts +++ b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsNotEmpty, IsArray } from 'class-validator'; +import { IsString, IsNotEmpty, IsArray, ArrayNotEmpty } from 'class-validator'; export class CreateTenantSchemaDto { @ApiProperty() @@ -19,11 +19,9 @@ export class CreateTenantSchemaDto { @ApiProperty() @IsArray({ message: 'attributes must be an array' }) - @IsString({ each: true }) - // TODO: IsNotEmpty won't work for array. Must use @ArrayNotEmpty() instead - // @ArrayNotEmpty({ message: 'please provide at least one attribute' }) - // @IsNotEmpty({ each: true, message: 'attribute must not be empty' }) - @IsNotEmpty({ message: 'please provide valid attributes' }) + @ArrayNotEmpty({ message: 'please provide at least one attribute' }) + @IsString({ each: true, message: 'each attribute must be a string' }) + @IsNotEmpty({ each: true, message: 'attribute must not be empty' }) attributes: string[]; @ApiProperty() diff --git a/apps/api-gateway/src/dtos/issue-credential-offer.dto .ts b/apps/api-gateway/src/dtos/issue-credential-offer.dto .ts deleted file mode 100644 index 9eed1f5ed..000000000 --- a/apps/api-gateway/src/dtos/issue-credential-offer.dto .ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsNotEmpty, IsBoolean, IsNotEmptyObject, IsObject } from 'class-validator'; -interface ICredAttrSpec { - 'mime-type': string, - name: string, - value: string -} - -interface ICredentialPreview { - '@type': string, - attributes: ICredAttrSpec[] -} - -export class IssueCredentialOfferDto { - - @ApiProperty({ example: true }) - @IsNotEmpty({message:'Please provide valid auto-issue'}) - @IsBoolean({message:'Auto-issue should be boolean'}) - auto_issue: boolean; - - @ApiProperty({ example: true }) - @IsNotEmpty({message:'Please provide valid auto-remove'}) - @IsBoolean({message:'Auto-remove should be boolean'}) - auto_remove: boolean; - - @ApiProperty({ example: 'comments' }) - @IsNotEmpty({message:'Please provide valid comment'}) - @IsString({message:'Comment should be string'}) - comment: string; - - @ApiProperty({ example: 'WgWxqztrNooG92RXvxSTWv:3:CL:20:tag' }) - @IsNotEmpty({message:'Please provide valid cred-def-id'}) - @IsString({message:'Cred-def-id should be string'}) - cred_def_id: string; - - @ApiProperty({ example: '3fa85f64-5717-4562-b3fc-2c963f66afa6' }) - @IsNotEmpty({message:'Please provide valid connection-id'}) - @IsString({message:'Connection-id should be string'}) - connection_id: string; - - @ApiProperty({ example: false }) - @IsNotEmpty({message:'Please provide valid trace'}) - @IsBoolean({message:'Trace should be boolean'}) - trace: boolean; - - - @ApiProperty({ - example: { - '@type': 'issue-credential/1.0/credential-preview', - 'attributes': [ - { - 'mime-type': 'image/jpeg', - 'name': 'favourite_drink', - 'value': 'martini' - } - ] - } - } - ) - - @IsObject({message:'Credential-preview should be object'}) - credential_preview: ICredentialPreview; -} - diff --git a/apps/api-gateway/src/helper-files/file-operation.helper.ts b/apps/api-gateway/src/helper-files/file-operation.helper.ts index 15ace62be..b5ab26542 100644 --- a/apps/api-gateway/src/helper-files/file-operation.helper.ts +++ b/apps/api-gateway/src/helper-files/file-operation.helper.ts @@ -1,18 +1,18 @@ import { promisify } from 'util'; import * as fs from 'fs'; -export const createFile = async (path: string, fileName: string, data: string): Promise => { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - if (!checkIfFileOrDirectoryExists(path)) { - fs.mkdirSync(path, { recursive: true }); +export const checkIfFileOrDirectoryExists = (path: string): boolean => fs.existsSync(path); + +const writeFile = promisify(fs.writeFile); +const mkdir = promisify(fs.mkdir); + +export const createFile = async (dirPath: string, fileName: string, data: string): Promise => { + if (!checkIfFileOrDirectoryExists(dirPath)) { + await mkdir(dirPath, { recursive: true }); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const writeFile = promisify(fs.writeFile); - return fs.writeFileSync(`${path}/${fileName}`, data, 'utf8'); + await writeFile(`${dirPath}/${fileName}`, data, 'utf8'); }; -export const checkIfFileOrDirectoryExists = (path: string): boolean => fs.existsSync(path); - export const getFile = async (path: string, encoding: BufferEncoding): Promise => { const readFile = promisify(fs.readFile); diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index 17e2dfbe9..5fd03a93c 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -1,6 +1,3 @@ -/* eslint-disable camelcase */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -// TODO: Remove this import { Injectable, Inject } from '@nestjs/common'; import { BaseService } from 'libs/service/base.service'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; diff --git a/libs/context/src/contextInterceptorModule.ts b/libs/context/src/contextInterceptorModule.ts index e9ff13139..c978ac2de 100644 --- a/libs/context/src/contextInterceptorModule.ts +++ b/libs/context/src/contextInterceptorModule.ts @@ -15,8 +15,8 @@ import NestjsClsContextStorageService from './nestjsClsContextStorageService'; generateId: true, idGenerator: (context: ExecutionContext) => { + const logger = new Logger('ContextInterceptorModule'); try { - const logger = new Logger('ContextInterceptorModule'); const rpcContext = context.switchToRpc().getContext(); const headers = rpcContext.getHeaders() ?? {}; const contextId = headers.get?.('contextId'); @@ -34,7 +34,8 @@ import NestjsClsContextStorageService from './nestjsClsContextStorageService'; } } catch (error) { // eslint-disable-next-line no-console - console.log('[idGenerator] Error in idGenerator: ', error); + logger.error('[idGenerator] Error in idGenerator, generating fallback UUID', error); + return uuid(); } } } diff --git a/libs/context/src/contextModule.ts b/libs/context/src/contextModule.ts index a03d046d1..a56df5510 100644 --- a/libs/context/src/contextModule.ts +++ b/libs/context/src/contextModule.ts @@ -4,6 +4,7 @@ import { ClsModule } from 'nestjs-cls'; import { ContextStorageServiceKey } from './contextStorageService.interface'; import NestjsClsContextStorageService from './nestjsClsContextStorageService'; +import { Request } from 'express'; @Global() @Module({ diff --git a/libs/logger/src/logging.interceptor.ts b/libs/logger/src/logging.interceptor.ts index 74b7db289..953528e2d 100644 --- a/libs/logger/src/logging.interceptor.ts +++ b/libs/logger/src/logging.interceptor.ts @@ -7,10 +7,6 @@ import Logger, { LoggerKey } from './logger.interface'; import { ClsService } from 'nestjs-cls'; import { v4 } from 'uuid'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isNullUndefinedOrEmpty = (obj: any): boolean => - null === obj || obj === undefined || ('object' === typeof obj && 0 === Object.keys(obj).length); - @Injectable() export class LoggingInterceptor implements NestInterceptor { constructor( @@ -24,18 +20,18 @@ export class LoggingInterceptor implements NestInterceptor { return this.clsService.run(() => { this.logger.info('In LoggingInterceptor configuration'); const rpcContext = context.switchToRpc().getContext(); - const headers = rpcContext.getHeaders(); + const headers = rpcContext.getHeaders?.() ?? rpcContext.getHeaders; + const contextIdFromHeader = headers && 'function' === typeof headers.get ? headers.get('contextId') : undefined; - if (!isNullUndefinedOrEmpty(headers)) { - this.logger.debug('We found context Id in header of logger Interceptor', headers.get('contextId')); - this.contextStorageService.setContextId(headers.get('contextId')); + if (contextIdFromHeader) { + this.logger.debug('We found context Id in header of logger Interceptor', contextIdFromHeader); + this.setupContextId(contextIdFromHeader); } else { const newContextId = v4(); - this.logger.debug('Not found context Id in header of logger Interceptor, generating a new one: ', newContextId); - this.contextStorageService.set('x-correlation-id', newContextId); - this.contextStorageService.set('contextId', newContextId); - this.contextStorageService.setContextId(newContextId); + this.logger.debug('Not found context Id in header of logger Interceptor, generating a new one:', newContextId); + this.setupContextId(newContextId); } + return next.handle().pipe( catchError((err) => { this.logger.error('[intercept] Error in LoggingInterceptor', err); @@ -44,4 +40,10 @@ export class LoggingInterceptor implements NestInterceptor { ); }); } + + private setupContextId(contextIdFromHeader: string | undefined): void { + this.contextStorageService.set('x-correlation-id', contextIdFromHeader); + this.contextStorageService.set('contextId', contextIdFromHeader); + this.contextStorageService.setContextId(contextIdFromHeader); + } } From dddc4be342b767fb17034ddd7e4a3a74f2c2c584 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Tue, 18 Nov 2025 13:52:49 +0530 Subject: [PATCH 39/43] fix: fixed coderabbitai suggestions and PR comments Signed-off-by: Rinkal Bhojani --- Dockerfiles/Dockerfile.oid4vc-issuance | 6 +- Dockerfiles/Dockerfile.oid4vc-verification | 7 +- .../src/agent-service.controller.ts | 2 +- .../src/agent-service.service.ts | 8 +- .../agent-service/dto/create-schema.dto.ts | 1 + .../src/connection/connection.service.ts | 4 +- .../src/connection/dtos/connection.dto.ts | 657 +++++++++--------- .../src/connection/enums/connections.enum.ts | 4 - .../dtos/issuer-sessions.dto.ts | 18 - apps/api-gateway/src/x509/x509.controller.ts | 6 +- apps/api-gateway/src/x509/x509.module.ts | 7 +- .../src/oid4vc-issuance.service.ts | 2 +- apps/x509/src/x509.service.ts | 2 +- libs/common/src/interfaces/x509.interface.ts | 2 +- libs/enum/src/enum.ts | 5 + libs/logger/src/winstonLogger.ts | 31 +- 16 files changed, 380 insertions(+), 382 deletions(-) delete mode 100644 apps/api-gateway/src/connection/enums/connections.enum.ts diff --git a/Dockerfiles/Dockerfile.oid4vc-issuance b/Dockerfiles/Dockerfile.oid4vc-issuance index ee861dc93..c3ac588a2 100644 --- a/Dockerfiles/Dockerfile.oid4vc-issuance +++ b/Dockerfiles/Dockerfile.oid4vc-issuance @@ -9,7 +9,6 @@ WORKDIR /app # Copy package.json and package-lock.json COPY package.json ./ COPY pnpm-workspace.yaml ./ -#COPY package-lock.json ./ ENV PUPPETEER_SKIP_DOWNLOAD=true @@ -18,7 +17,6 @@ RUN pnpm i --ignore-scripts # Copy the rest of the application code COPY . . -# RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate RUN cd libs/prisma-service && npx prisma generate # Build the oid4vc-issuance service @@ -29,7 +27,7 @@ RUN npm run build oid4vc-issuance FROM node:18-alpine # Install OpenSSL RUN apk add --no-cache openssl -# RUN npm install -g pnpm + # Set the working directory WORKDIR /app @@ -38,7 +36,7 @@ COPY --from=build /app/dist/apps/oid4vc-issuance/ ./dist/apps/oid4vc-issuance/ # Copy the libs folder from the build stage COPY --from=build /app/libs/ ./libs/ -#COPY --from=build /app/package.json ./ + COPY --from=build /app/node_modules ./node_modules # Set the command to run the microservice diff --git a/Dockerfiles/Dockerfile.oid4vc-verification b/Dockerfiles/Dockerfile.oid4vc-verification index 97c13e087..3ae0eea10 100644 --- a/Dockerfiles/Dockerfile.oid4vc-verification +++ b/Dockerfiles/Dockerfile.oid4vc-verification @@ -9,7 +9,6 @@ WORKDIR /app # Copy package.json and package-lock.json COPY package.json ./ COPY pnpm-workspace.yaml ./ -#COPY package-lock.json ./ ENV PUPPETEER_SKIP_DOWNLOAD=true @@ -18,7 +17,7 @@ RUN pnpm i --ignore-scripts # Copy the rest of the application code COPY . . -# RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate + RUN cd libs/prisma-service && npx prisma generate # Build the oid4vc-verification service @@ -29,7 +28,7 @@ RUN npm run build oid4vc-verification FROM node:18-alpine # Install OpenSSL RUN apk add --no-cache openssl -# RUN npm install -g pnpm + # Set the working directory WORKDIR /app @@ -38,7 +37,7 @@ COPY --from=build /app/dist/apps/oid4vc-verification/ ./dist/apps/oid4vc-verific # Copy the libs folder from the build stage COPY --from=build /app/libs/ ./libs/ -#COPY --from=build /app/package.json ./ + COPY --from=build /app/node_modules ./node_modules # Set the command to run the microservice diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index 36d132d1e..bfe047476 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -372,7 +372,7 @@ export class AgentServiceController { @MessagePattern({ cmd: 'agent-service-oid4vc-get-credential-offer-by-id' }) // eslint-disable-next-line @typescript-eslint/no-explicit-any - async oidcGetCredentialOfferById(payload: { url: string; orgId: string; offerId: string }): Promise { + async oidcGetCredentialOfferById(payload: { url: string; orgId: string }): Promise { return this.agentServiceService.oidcGetCredentialOfferById(payload.url, payload.orgId); } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index 648e64b9e..d14d43fe9 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -2201,7 +2201,12 @@ export class AgentServiceService { private async tokenEncryption(token: string): Promise { try { - const encryptedToken = CryptoJS.AES.encrypt(JSON.stringify(token), process.env.CRYPTO_PRIVATE_KEY).toString(); + const secret = process.env.CRYPTO_PRIVATE_KEY; + if (!secret) { + this.logger.error('CRYPTO_PRIVATE_KEY is not configured'); + throw new InternalServerErrorException('Encryption key is not configured'); + } + const encryptedToken = CryptoJS.AES.encrypt(JSON.stringify(token), secret).toString(); return encryptedToken; } catch (error) { @@ -2312,6 +2317,7 @@ export class AgentServiceService { this.logger.error( `[getOid4vpVerifierSession] Error in getting oid4vp verifier session in agent service : ${JSON.stringify(error)}` ); + throw error; } } diff --git a/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts index 5cc00c285..76144b811 100644 --- a/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts +++ b/apps/api-gateway/src/agent-service/dto/create-schema.dto.ts @@ -25,6 +25,7 @@ export class CreateTenantSchemaDto { attributes: string[]; @ApiProperty() + @IsString({ message: 'orgId must be a string' }) @IsNotEmpty({ message: 'please provide orgId' }) orgId: string; } diff --git a/apps/api-gateway/src/connection/connection.service.ts b/apps/api-gateway/src/connection/connection.service.ts index d08d181ca..9f0f42c92 100644 --- a/apps/api-gateway/src/connection/connection.service.ts +++ b/apps/api-gateway/src/connection/connection.service.ts @@ -44,7 +44,7 @@ export class ConnectionService extends BaseService { basicMessageDto ); } catch (error) { - throw new RpcException(error.response); + throw new RpcException(error?.response ?? error); } } @@ -60,7 +60,7 @@ export class ConnectionService extends BaseService { const connectionDetails = { referenceId }; return this.natsClient.sendNats(this.connectionServiceProxy, 'get-connection-url', connectionDetails); } catch (error) { - throw new RpcException(error.response); + throw new RpcException(error?.response ?? error); } } diff --git a/apps/api-gateway/src/connection/dtos/connection.dto.ts b/apps/api-gateway/src/connection/dtos/connection.dto.ts index da4cef77d..3720426f1 100644 --- a/apps/api-gateway/src/connection/dtos/connection.dto.ts +++ b/apps/api-gateway/src/connection/dtos/connection.dto.ts @@ -1,369 +1,376 @@ -import { ArrayNotEmpty, IsArray, IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, IsUrl, ValidateNested } from 'class-validator'; +import { + ArrayNotEmpty, + IsArray, + IsBoolean, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + IsUrl, + ValidateNested +} from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { HandshakeProtocol } from '../enums/connections.enum'; import { IsNotSQLInjection } from '@credebl/common/cast.helper'; +import { HandshakeProtocol } from '@credebl/enum/enum'; export class CreateOutOfBandConnectionInvitation { - @ApiPropertyOptional() - @IsOptional() - label?: string; - - @ApiPropertyOptional() - @IsOptional() - alias?: string; - - @ApiPropertyOptional() - @IsOptional() - imageUrl?: string; - - @ApiPropertyOptional() - @IsOptional() - goalCode?: string; - - @ApiPropertyOptional() - @IsOptional() - goal?: string; - - @ApiPropertyOptional() - @IsOptional() - handshake?: boolean; - - @ApiPropertyOptional() - @IsOptional() - handshakeProtocols?: HandshakeProtocol[]; - - @ApiPropertyOptional() - @IsOptional() - messages?: object[]; - - @ApiPropertyOptional() - @IsOptional() - multiUseInvitation?: boolean; - - @ApiPropertyOptional() - @IsOptional() - IsReuseConnection?: boolean; - - @ApiPropertyOptional() - @IsOptional() - autoAcceptConnection?: boolean; - - @ApiPropertyOptional() - @IsOptional() - routing?: object; - - @ApiPropertyOptional() - @IsOptional() - appendedAttachments?: object[]; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide recipientKey' }) - recipientKey: string; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide invitation did' }) - invitationDid?: string; - - orgId; + @ApiPropertyOptional() + @IsOptional() + label?: string; + + @ApiPropertyOptional() + @IsOptional() + alias?: string; + + @ApiPropertyOptional() + @IsOptional() + imageUrl?: string; + + @ApiPropertyOptional() + @IsOptional() + goalCode?: string; + + @ApiPropertyOptional() + @IsOptional() + goal?: string; + + @ApiPropertyOptional() + @IsOptional() + handshake?: boolean; + + @ApiPropertyOptional() + @IsOptional() + handshakeProtocols?: HandshakeProtocol[]; + + @ApiPropertyOptional() + @IsOptional() + messages?: object[]; + + @ApiPropertyOptional() + @IsOptional() + multiUseInvitation?: boolean; + + @ApiPropertyOptional() + @IsOptional() + IsReuseConnection?: boolean; + + @ApiPropertyOptional() + @IsOptional() + autoAcceptConnection?: boolean; + + @ApiPropertyOptional() + @IsOptional() + routing?: object; + + @ApiPropertyOptional() + @IsOptional() + appendedAttachments?: object[]; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide recipientKey' }) + recipientKey: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide invitation did' }) + invitationDid?: string; + + orgId; } export class CreateConnectionDto { - @ApiPropertyOptional() - @IsOptional() - @IsString({ message: 'alias must be a string' }) - @IsNotEmpty({ message: 'please provide valid alias' }) - @IsNotSQLInjection({ message: 'alias is required.' }) - alias: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ message: 'label must be a string' }) - @IsNotEmpty({ message: 'please provide valid label' }) - @IsNotSQLInjection({ message: 'label is required.' }) - label: string; - - @ApiPropertyOptional() - @IsOptional() - @IsNotEmpty({ message: 'please provide valid imageUrl' }) - @IsString({ message: 'imageUrl must be a string' }) - imageUrl: string; - - @ApiPropertyOptional() - @IsBoolean() - @IsOptional() - @IsNotEmpty({ message: 'please provide multiUseInvitation' }) - multiUseInvitation: boolean; - - @ApiPropertyOptional() - @IsBoolean() - @IsOptional() - @IsNotEmpty({ message: 'Please provide autoAcceptConnection' }) - autoAcceptConnection: boolean; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide goalCode' }) - goalCode: string; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide goal' }) - goal: string; - - @ApiPropertyOptional() - @IsBoolean() - @IsOptional() - @IsNotEmpty({ message: 'Please provide handshake' }) - handshake: boolean; - - @ApiPropertyOptional() - @IsArray() - @ArrayNotEmpty() - @IsOptional() - @IsString({ each: true }) - handshakeProtocols: string[]; - - orgId: string; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide recipientKey' }) - recipientKey: string; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'Please provide invitation did' }) - invitationDid?: string; + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'alias must be a string' }) + @IsNotEmpty({ message: 'please provide valid alias' }) + @IsNotSQLInjection({ message: 'alias is required.' }) + alias: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'label must be a string' }) + @IsNotEmpty({ message: 'please provide valid label' }) + @IsNotSQLInjection({ message: 'label is required.' }) + label: string; + + @ApiPropertyOptional() + @IsOptional() + @IsNotEmpty({ message: 'please provide valid imageUrl' }) + @IsString({ message: 'imageUrl must be a string' }) + imageUrl: string; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'please provide multiUseInvitation' }) + multiUseInvitation: boolean; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'Please provide autoAcceptConnection' }) + autoAcceptConnection: boolean; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide goalCode' }) + goalCode: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide goal' }) + goal: string; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + @IsNotEmpty({ message: 'Please provide handshake' }) + handshake: boolean; + + @ApiPropertyOptional() + @IsArray() + @ArrayNotEmpty() + @IsOptional() + @IsString({ each: true }) + handshakeProtocols: string[]; + + orgId: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide recipientKey' }) + recipientKey: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'Please provide invitation did' }) + invitationDid?: string; } export class ConnectionDto { - @ApiPropertyOptional() - @IsOptional() - id: string; + @ApiPropertyOptional() + @IsOptional() + id: string; - @ApiPropertyOptional() - @IsOptional() - createdAt: string; + @ApiPropertyOptional() + @IsOptional() + createdAt: string; - @ApiPropertyOptional() - @IsOptional() - did: string; + @ApiPropertyOptional() + @IsOptional() + did: string; - @ApiPropertyOptional() - @IsOptional() - theirDid: string; + @ApiPropertyOptional() + @IsOptional() + theirDid: string; - @ApiPropertyOptional() - @IsOptional() - theirLabel: string; + @ApiPropertyOptional() + @IsOptional() + theirLabel: string; - @ApiPropertyOptional() - @IsOptional() - state: string; + @ApiPropertyOptional() + @IsOptional() + state: string; - @ApiPropertyOptional() - @IsOptional() - role: string; + @ApiPropertyOptional() + @IsOptional() + role: string; - @ApiPropertyOptional() - @IsOptional() - imageUrl: string; + @ApiPropertyOptional() + @IsOptional() + imageUrl: string; - @ApiPropertyOptional() - @IsOptional() - autoAcceptConnection: boolean; + @ApiPropertyOptional() + @IsOptional() + autoAcceptConnection: boolean; - @ApiPropertyOptional() - @IsOptional() - threadId: string; + @ApiPropertyOptional() + @IsOptional() + threadId: string; - @ApiPropertyOptional() - @IsOptional() - protocol: string; + @ApiPropertyOptional() + @IsOptional() + protocol: string; - @ApiPropertyOptional() - @IsOptional() - outOfBandId: string; + @ApiPropertyOptional() + @IsOptional() + outOfBandId: string; - @ApiPropertyOptional() - @IsOptional() - updatedAt: string; + @ApiPropertyOptional() + @IsOptional() + updatedAt: string; - @ApiPropertyOptional() - @IsOptional() - contextCorrelationId: string; + @ApiPropertyOptional() + @IsOptional() + contextCorrelationId: string; - @ApiPropertyOptional() - @IsOptional() - type: string; + @ApiPropertyOptional() + @IsOptional() + type: string; - @ApiPropertyOptional() - @IsOptional() - orgId: string; + @ApiPropertyOptional() + @IsOptional() + orgId: string; - @ApiPropertyOptional() - @IsOptional() - outOfBandRecord?: object; + @ApiPropertyOptional() + @IsOptional() + outOfBandRecord?: object; - @ApiPropertyOptional() - @IsOptional() - reuseThreadId?: string; + @ApiPropertyOptional() + @IsOptional() + reuseThreadId?: string; } class ReceiveInvitationCommonDto { - @ApiPropertyOptional() - @IsOptional() - @IsString({ message: 'alias must be a string' }) - @IsNotEmpty({ message: 'please provide valid alias' }) - alias: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ message: 'label must be a string' }) - @IsNotEmpty({ message: 'please provide valid label' }) - label: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ message: 'imageUrl must be a string' }) - @IsNotEmpty({ message: 'please provide valid imageUrl' }) - @IsString() - imageUrl: string; - - @ApiPropertyOptional() - @IsOptional() - @IsBoolean({ message: 'autoAcceptConnection must be a boolean' }) - @IsNotEmpty({ message: 'please provide valid autoAcceptConnection' }) - autoAcceptConnection: boolean; - - @ApiPropertyOptional() - @IsOptional() - @IsBoolean({ message: 'autoAcceptInvitation must be a boolean' }) - @IsNotEmpty({ message: 'please provide valid autoAcceptInvitation' }) - autoAcceptInvitation: boolean; - - @ApiPropertyOptional() - @IsOptional() - @IsBoolean({ message: 'reuseConnection must be a boolean' }) - @IsNotEmpty({ message: 'please provide valid reuseConnection' }) - reuseConnection: boolean; - - @ApiPropertyOptional() - @IsOptional() - @IsNumber() - @IsNotEmpty({ message: 'please provide valid acceptInvitationTimeoutMs' }) - acceptInvitationTimeoutMs: number; + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'alias must be a string' }) + @IsNotEmpty({ message: 'please provide valid alias' }) + alias: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'label must be a string' }) + @IsNotEmpty({ message: 'please provide valid label' }) + label: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'imageUrl must be a string' }) + @IsNotEmpty({ message: 'please provide valid imageUrl' }) + @IsString() + imageUrl: string; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean({ message: 'autoAcceptConnection must be a boolean' }) + @IsNotEmpty({ message: 'please provide valid autoAcceptConnection' }) + autoAcceptConnection: boolean; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean({ message: 'autoAcceptInvitation must be a boolean' }) + @IsNotEmpty({ message: 'please provide valid autoAcceptInvitation' }) + autoAcceptInvitation: boolean; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean({ message: 'reuseConnection must be a boolean' }) + @IsNotEmpty({ message: 'please provide valid reuseConnection' }) + reuseConnection: boolean; + + @ApiPropertyOptional() + @IsOptional() + @IsNumber() + @IsNotEmpty({ message: 'please provide valid acceptInvitationTimeoutMs' }) + acceptInvitationTimeoutMs: number; } export class ReceiveInvitationUrlDto extends ReceiveInvitationCommonDto { - - @ApiProperty() - @IsOptional() - @IsString({ message: 'invitationUrl must be a string' }) - @IsNotEmpty({ message: 'please provide valid invitationUrl' }) - invitationUrl: string; + @ApiProperty() + @IsOptional() + @IsString({ message: 'invitationUrl must be a string' }) + @IsNotEmpty({ message: 'please provide valid invitationUrl' }) + invitationUrl: string; } - class ServiceDto { - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'please provide valid id' }) - id: string; - - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'please provide valid serviceEndpoint' }) - @IsUrl({}, { message: 'Invalid serviceEndpoint format' }) - serviceEndpoint: string; - - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'please provide valid type' }) - type: string; - - @ApiProperty() - @IsString({ each: true }) - recipientKeys: string[]; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ each: true }) - routingKeys: string[]; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ each: true }) - accept: string[]; + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'please provide valid id' }) + id: string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'please provide valid serviceEndpoint' }) + @IsUrl({}, { message: 'Invalid serviceEndpoint format' }) + serviceEndpoint: string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'please provide valid type' }) + type: string; + + @ApiProperty() + @IsString({ each: true }) + recipientKeys: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ each: true }) + routingKeys: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ each: true }) + accept: string[]; } class InvitationDto { - @ApiPropertyOptional() - @IsOptional() - @IsString() - @IsNotEmpty({ message: 'please provide valid @id' }) - '@id': string; - - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'please provide valid @type' }) - '@type': string; - - @ApiProperty() - @IsString() - @IsNotEmpty({ message: 'please provide valid label' }) - label: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString() - @IsNotEmpty({ message: 'please provide valid goalCode' }) - goalCode: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString() - @IsNotEmpty({ message: 'please provide valid goal' }) - goal: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ each: true }) - accept: string[]; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ each: true }) - // eslint-disable-next-line camelcase - handshake_protocols: string[]; - - @ApiProperty() - @ValidateNested({ each: true }) - @Type(() => ServiceDto) - services: ServiceDto[]; - - @ApiPropertyOptional() - @IsString() - @IsOptional() - @IsNotEmpty({ message: 'please provide valid imageUrl' }) - @IsString() - imageUrl?: string; + @ApiPropertyOptional() + @IsOptional() + @IsString() + @IsNotEmpty({ message: 'please provide valid @id' }) + '@id': string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'please provide valid @type' }) + '@type': string; + + @ApiProperty() + @IsString() + @IsNotEmpty({ message: 'please provide valid label' }) + label: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + @IsNotEmpty({ message: 'please provide valid goalCode' }) + goalCode: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + @IsNotEmpty({ message: 'please provide valid goal' }) + goal: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ each: true }) + accept: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ each: true }) + // eslint-disable-next-line camelcase + handshake_protocols: string[]; + + @ApiProperty() + @ValidateNested({ each: true }) + @Type(() => ServiceDto) + services: ServiceDto[]; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + @IsNotEmpty({ message: 'please provide valid imageUrl' }) + @IsString() + imageUrl?: string; } export class ReceiveInvitationDto extends ReceiveInvitationCommonDto { - - @ApiProperty() - @ValidateNested() - @Type(() => InvitationDto) - invitation: InvitationDto; -} \ No newline at end of file + @ApiProperty() + @ValidateNested() + @Type(() => InvitationDto) + invitation: InvitationDto; +} diff --git a/apps/api-gateway/src/connection/enums/connections.enum.ts b/apps/api-gateway/src/connection/enums/connections.enum.ts deleted file mode 100644 index 9c76afc13..000000000 --- a/apps/api-gateway/src/connection/enums/connections.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export declare enum HandshakeProtocol { - Connections = 'https://didcomm.org/connections/1.0', - DidExchange = 'https://didcomm.org/didexchange/1.0' -} 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 fc2996c6f..d32d1a95f 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 @@ -164,15 +164,6 @@ export class CredentialRequestDto { }) @IsOptional() validityInfo?: ValidityInfo; - - // @ApiPropertyOptional({ - // description: 'Selective disclosure: claim -> boolean (or nested map)', - // example: { name: true, DOB: true, additionalProp3: false }, - // required: false - // }) - // @IsOptional() - // @IsDisclosureFrame() - // disclosureFrame?: Record>; } export class CreateOidcCredentialOfferDto { @@ -207,15 +198,6 @@ export class GetAllCredentialOfferDto { @IsOptional() preAuthorizedCode: string = ''; - // @ApiPropertyOptional({ - // example: OpenId4VcIssuanceSessionState.OfferCreated, - // enum: OpenId4VcIssuanceSessionState, - // required: false, - // }) - // @IsOptional() - // @IsEnum(OpenId4VcIssuanceSessionState) - // state?: OpenId4VcIssuanceSessionState; - @ApiProperty({ required: false, example: 'openid-credential-offer://?credential_offer_uri=http%3A%2F%2.....' }) @IsOptional() credentialOfferUri: string = ''; diff --git a/apps/api-gateway/src/x509/x509.controller.ts b/apps/api-gateway/src/x509/x509.controller.ts index e413071fa..a572cdedd 100644 --- a/apps/api-gateway/src/x509/x509.controller.ts +++ b/apps/api-gateway/src/x509/x509.controller.ts @@ -7,7 +7,6 @@ import { ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; -import { CommonService } from '@credebl/common'; import { Controller, Get, @@ -53,10 +52,7 @@ import { SortFields, x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; @ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) @ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) export class X509Controller { - constructor( - private readonly x509Service: X509Service, - private readonly commonService: CommonService - ) {} + constructor(private readonly x509Service: X509Service) {} /** * Create a new x509 diff --git a/apps/api-gateway/src/x509/x509.module.ts b/apps/api-gateway/src/x509/x509.module.ts index 7a4394ca1..abcb35365 100644 --- a/apps/api-gateway/src/x509/x509.module.ts +++ b/apps/api-gateway/src/x509/x509.module.ts @@ -1,5 +1,3 @@ -import { CommonModule, CommonService } from '@credebl/common'; - import { ClientsModule, Transport } from '@nestjs/microservices'; import { ConfigModule } from '@nestjs/config'; import { HttpModule } from '@nestjs/axios'; @@ -19,11 +17,10 @@ import { NATSClient } from '@credebl/common/NATSClient'; name: 'NATS_CLIENT', transport: Transport.NATS, options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.API_GATEWAY_NKEY_SEED) - }, - CommonModule + } ]) ], controllers: [X509Controller], - providers: [X509Service, CommonService, AwsService, NATSClient] + providers: [X509Service, AwsService, NATSClient] }) export class X509Module {} diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 5509bf548..245ff5f7c 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -724,7 +724,7 @@ export class Oid4vcIssuanceService { try { const url = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.OIDC_ISSUER_SESSIONS); const credentialOfferUrl = buildCredentialOfferUrl(url, getAllCredentialOffer); - const offers = await this._oidcGetCredentialOfferById(credentialOfferUrl, orgId); + const offers = await this._oidcGetCredentialOffers(credentialOfferUrl, orgId); if ('string' === typeof offers.response) { offers.response = JSON.parse(offers.response); } diff --git a/apps/x509/src/x509.service.ts b/apps/x509/src/x509.service.ts index c9c92bdfc..b4a3c4659 100644 --- a/apps/x509/src/x509.service.ts +++ b/apps/x509/src/x509.service.ts @@ -321,7 +321,7 @@ export class X509CertificateService extends BaseService { status: error.statusCode, error: error.message }, - error.error + error.statusCode ); }); } catch (error) { diff --git a/libs/common/src/interfaces/x509.interface.ts b/libs/common/src/interfaces/x509.interface.ts index 17c350c22..05ee04f88 100644 --- a/libs/common/src/interfaces/x509.interface.ts +++ b/libs/common/src/interfaces/x509.interface.ts @@ -1,4 +1,4 @@ -import { X509ExtendedKeyUsage, X509KeyUsage, x5cKeyType } from '@credebl/enum/enum'; +import { X509ExtendedKeyUsage, X509KeyUsage, x5cKeyType, KeyType } from '@credebl/enum/enum'; // Enum remains the same export enum GeneralNameType { diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 3893b7411..2c9829deb 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -337,3 +337,8 @@ export enum SignerMethodOption { DID = 'did', X5C = 'x5c' } + +export declare enum HandshakeProtocol { + Connections = 'https://didcomm.org/connections/1.0', + DidExchange = 'https://didcomm.org/didexchange/1.0' +} diff --git a/libs/logger/src/winstonLogger.ts b/libs/logger/src/winstonLogger.ts index ed8004cd0..b9bbb165d 100644 --- a/libs/logger/src/winstonLogger.ts +++ b/libs/logger/src/winstonLogger.ts @@ -8,17 +8,28 @@ import * as ecsFormat from '@elastic/ecs-winston-format'; export const WinstonLoggerTransportsKey = Symbol(); let esTransport; if ('true' === process.env.ELK_LOG?.toLowerCase()) { - const esTransportOpts = { - level: `${process.env.LOG_LEVEL}`, - clientOpts: { - node: `${process.env.ELK_LOG_PATH}`, - auth: { - username: `${process.env.ELK_USERNAME}`, - password: `${process.env.ELK_PASSWORD}` + const requiredVars = ['LOG_LEVEL', 'ELK_LOG_PATH', 'ELK_USERNAME', 'ELK_PASSWORD']; + const missingVars = requiredVars.filter((v) => !process.env[v]); + if (0 < missingVars.length) { + // eslint-disable-next-line no-console + console.warn(`Elasticsearch logging disabled: missing env vars [${missingVars.join(', ')}]`); + } else { + const esTransportOpts = { + level: `${process.env.LOG_LEVEL}`, + clientOpts: { + node: `${process.env.ELK_LOG_PATH}`, + auth: { + username: `${process.env.ELK_USERNAME}`, + password: `${process.env.ELK_PASSWORD}` + } } - } - }; - esTransport = new Elasticsearch.ElasticsearchTransport(esTransportOpts); + }; + esTransport = new Elasticsearch.ElasticsearchTransport(esTransportOpts); + esTransport.on('error', (error) => { + // eslint-disable-next-line no-console + console.error('Elasticsearch transport error:', error); + }); + } } @Injectable() From 24f835cb4499ecc52ded3f9195f89508b670cb80 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Wed, 19 Nov 2025 11:34:16 +0530 Subject: [PATCH 40/43] fix: fixed minor issues Signed-off-by: Rinkal Bhojani --- Dockerfiles/Dockerfile.x509 | 8 +++----- .../src/oid4vc-issuance/oid4vc-issuance.controller.ts | 2 +- .../dtos/oid4vc-verifier-presentation.dto.ts | 3 ++- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Dockerfiles/Dockerfile.x509 b/Dockerfiles/Dockerfile.x509 index 6d15a054f..08207f431 100644 --- a/Dockerfiles/Dockerfile.x509 +++ b/Dockerfiles/Dockerfile.x509 @@ -6,10 +6,8 @@ RUN npm install -g pnpm # Set the working directory WORKDIR /app -# Copy package.json and package-lock.json COPY package.json ./ COPY pnpm-workspace.yaml ./ -#COPY package-lock.json ./ ENV PUPPETEER_SKIP_DOWNLOAD=true @@ -18,7 +16,7 @@ RUN pnpm i --ignore-scripts # Copy the rest of the application code COPY . . -# RUN cd libs/prisma-service && npx prisma migrate deploy && npx prisma generate + RUN cd libs/prisma-service && npx prisma generate # Build the x509 service @@ -29,7 +27,7 @@ RUN npm run build x509 FROM node:18-alpine # Install OpenSSL RUN apk add --no-cache openssl -# RUN npm install -g pnpm + # Set the working directory WORKDIR /app @@ -38,7 +36,7 @@ COPY --from=build /app/dist/apps/x509/ ./dist/apps/x509/ # Copy the libs folder from the build stage COPY --from=build /app/libs/ ./libs/ -#COPY --from=build /app/package.json ./ + COPY --from=build /app/node_modules ./node_modules # Set the command to run the microservice diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts index 79df3250d..567ef634c 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts @@ -197,7 +197,7 @@ export class Oid4vcIssuanceController { return res.status(HttpStatus.OK).json(finalResponse); } - @Delete('/orgs/:orgId/oid4vc/:id') + @Delete('/orgs/:orgId/oid4vc/issuers/:id') @ApiOperation({ summary: 'Delete OID4VC issuer', description: 'Deletes an OID4VC issuer for the specified organization.' 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 8c553997c..7d6113d09 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 @@ -1,5 +1,5 @@ import { OpenId4VcVerificationPresentationState } from '@credebl/common/interfaces/oid4vp-verification'; -import { ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiHideProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsEnum, IsOptional, IsString, IsUrl } from 'class-validator'; /* eslint-disable camelcase */ import { ApiProperty } from '@nestjs/swagger'; @@ -338,6 +338,7 @@ export class PresentationRequestDto { * This property is not serialized into requests/responses but is required so `class-validator` * executes the validator with access to the whole object. */ + @ApiHideProperty() @ApiPropertyOptional({ description: 'Internal: ensures exactly one of dcql or presentationExchange is present' }) From 92a9e36be31f7ccae5b3d83c5f63780ee50e7132 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Fri, 21 Nov 2025 11:20:46 +0530 Subject: [PATCH 41/43] fix: refactored code Signed-off-by: Rinkal Bhojani --- .../dtos/oid4vc-credential-wh.dto.ts | 13 ---------- .../oid4vc-issuance.controller.ts | 9 +++---- .../oid4vc-issuance.service.ts | 4 +-- .../oid4vc-verification.controller.ts | 6 ++--- apps/api-gateway/src/x509/dtos/x509.dto.ts | 2 +- apps/api-gateway/src/x509/x509.service.ts | 1 - .../helpers/credential-sessions.builder.ts | 25 ++++++++++++++++++- .../libs/helpers/issuer.metadata.ts | 2 +- .../src/oid4vc-issuance.service.ts | 5 +++- .../src/oid4vc-verification.service.ts | 2 +- apps/oid4vc-verification/test/app.e2e-spec.ts | 3 --- libs/common/src/common.constant.ts | 2 +- libs/common/src/common.utils.ts | 2 +- libs/common/src/response-messages/index.ts | 9 ++----- libs/logger/src/logging.interceptor.ts | 2 +- 15 files changed, 45 insertions(+), 42 deletions(-) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts index e36dbe1ed..7a450c021 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-credential-wh.dto.ts @@ -71,16 +71,3 @@ export function extractCredentialConfigurationIds(payload: Partial -): Partial { - const ids = extractCredentialConfigurationIds(payload); - return { - ...payload, - credentialOfferPayload: { - // eslint-disable-next-line camelcase - credential_configuration_ids: ids - } - }; -} diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts index 567ef634c..797909344 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.controller.ts @@ -46,7 +46,7 @@ import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler import { user } from '@prisma/client'; import { IssuerCreationDto, IssuerUpdationDto } from './dtos/oid4vc-issuer.dto'; import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oid4vc-issuer-template.dto'; -import { OidcIssueCredentialDto, sanitizeOidcIssueCredentialDto } from './dtos/oid4vc-credential-wh.dto'; +import { OidcIssueCredentialDto } from './dtos/oid4vc-credential-wh.dto'; import { Oid4vcIssuanceService } from './oid4vc-issuance.service'; import { CreateCredentialOfferD2ADto, @@ -428,7 +428,7 @@ export class Oid4vcIssuanceController { @User() user: user, @Res() res: Response ): Promise { - await this.oid4vcIssuanceService.deleteTemplate(user, orgId, templateId); + await this.oid4vcIssuanceService.deleteTemplate(user, orgId, templateId, issuerId); const finalResponse: IResponse = { statusCode: HttpStatus.OK, message: ResponseMessages.oidcTemplate.success.delete @@ -636,7 +636,7 @@ export class Oid4vcIssuanceController { if (id && 'default' === oidcIssueCredentialDto.contextCorrelationId) { oidcIssueCredentialDto.orgId = id; } - // const sanitized = sanitizeOidcIssueCredentialDto(oidcIssueCredentialDto); + const getCredentialDetails = await this.oid4vcIssuanceService.oidcIssueCredentialWebhook( oidcIssueCredentialDto, id @@ -653,9 +653,8 @@ export class Oid4vcIssuanceController { .catch((error) => { this.logger.debug(`error in getting webhook url ::: ${JSON.stringify(error)}`); }); - console.log(`webhookUrl `, webhookUrl); if (webhookUrl) { - console.log(`Org webhook found `, JSON.stringify(webhookUrl), JSON.stringify(oidcIssueCredentialDto)); + this.logger.log(`Posting response to the webhook url`); const plainIssuanceDto = JSON.parse(JSON.stringify(oidcIssueCredentialDto)); await this.oid4vcIssuanceService._postWebhookResponse(webhookUrl, { data: plainIssuanceDto }).catch((error) => { diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts index 01eae5e02..c5a208ede 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts @@ -53,8 +53,8 @@ export class Oid4vcIssuanceService extends BaseService { return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-delete-issuer', payload); } - async deleteTemplate(userDetails: user, orgId: string, templateId: string): Promise { - const payload = { templateId, orgId, userDetails }; + async deleteTemplate(userDetails: user, orgId: string, templateId: string, issuerId: string): Promise { + const payload = { templateId, orgId, userDetails, issuerId }; return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-template-delete', payload); } diff --git a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts index a87e373b1..12b36bf45 100644 --- a/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts +++ b/apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts @@ -71,7 +71,7 @@ export class Oid4vcVerificationController { summary: 'Create OID4VP verifier', description: 'Creates a new OID4VP verifier for the specified organization.' }) - @ApiResponse({ status: HttpStatus.OK, description: 'Verifier created successfully.', type: ApiResponseDto }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Verifier created successfully.', type: ApiResponseDto }) @ApiBearerAuth() @Roles(OrgRoles.OWNER) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @@ -151,7 +151,7 @@ export class Oid4vcVerificationController { message: ResponseMessages.oid4vp.success.update, data: updateVerifierRes }; - return res.status(HttpStatus.CREATED).json(finalResponse); + return res.status(HttpStatus.OK).json(finalResponse); } @Get('/orgs/:orgId/oid4vp/verifier') @@ -242,7 +242,7 @@ export class Oid4vcVerificationController { this.logger.debug(`[deleteVerifierDetails] Deleted verifier: ${verifierId}`); const finalResponse: IResponse = { statusCode: HttpStatus.OK, - message: ResponseMessages.oid4vp.success.fetch, + message: ResponseMessages.oid4vp.success.delete, data: verifierDetails }; return res.status(HttpStatus.OK).json(finalResponse); diff --git a/apps/api-gateway/src/x509/dtos/x509.dto.ts b/apps/api-gateway/src/x509/dtos/x509.dto.ts index 23548ecdb..595f81d33 100644 --- a/apps/api-gateway/src/x509/dtos/x509.dto.ts +++ b/apps/api-gateway/src/x509/dtos/x509.dto.ts @@ -306,7 +306,7 @@ export class BasicX509CreateCertificateConfig { @ApiPropertyOptional({ type: () => AuthorityAndSubjectKeyDto }) @IsOptional() - @ValidateNested({ each: true }) + @ValidateNested() @Type(() => AuthorityAndSubjectKeyDto) subjectKey?: AuthorityAndSubjectKeyDto; } diff --git a/apps/api-gateway/src/x509/x509.service.ts b/apps/api-gateway/src/x509/x509.service.ts index e3d82046d..e4ce19597 100644 --- a/apps/api-gateway/src/x509/x509.service.ts +++ b/apps/api-gateway/src/x509/x509.service.ts @@ -33,7 +33,6 @@ export class X509Service extends BaseService { reqUser: user ): Promise { this.logger.log(`Start creating x509 certficate`); - this.logger.debug(`payload : `, createDto, reqUser); const payload = { options: createDto, user: reqUser, orgId }; return this.natsClient.sendNatsMessage(this.serviceProxy, 'create-x509-certificate', payload); } diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index b2a6feff9..dbe75ad97 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -282,9 +282,22 @@ function buildSdJwtCredential( const templateSignerOption: SignerOption = signerOptions.find( (x) => templateRecord.signerOption.toLowerCase() === x.method ); + if (!templateSignerOption) { + throw new UnprocessableEntityException( + `Signer option "${templateRecord.signerOption}" is not configured for template ${templateRecord.id}` + ); + } if (templateRecord.signerOption === SignerMethodOption.X5C && credentialRequest.validityInfo) { - const certificateDetail = activeCertificateDetails.find((x) => x.certificateBase64 === templateSignerOption.x5c[0]); + 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, @@ -349,7 +362,17 @@ function buildMdocCredential( ) { throw new UnprocessableEntityException(`${ResponseMessages.oidcIssuerSession.error.missingValidityInfo}`); } + + if (!signerOptions?.length || !signerOptions[0].x5c?.length) { + throw new UnprocessableEntityException('An x5c signer configuration is required for mdoc credentials.'); + } + if (!activeCertificateDetails?.length) { + throw new UnprocessableEntityException('Active x.509 certificate details are required for mdoc credentials.'); + } const certificateDetail = activeCertificateDetails.find((x) => x.certificateBase64 === signerOptions[0].x5c[0]); + if (!certificateDetail) { + throw new UnprocessableEntityException('No active x.509 certificate matches the configured signer option.'); + } const validationResult = validateCredentialDatesInCertificateWindow( credentialRequest.validityInfo, certificateDetail diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index a8555607a..96f5282c0 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -174,7 +174,7 @@ export function buildIssuerPayload( return { display, dpopSigningAlgValuesSupported: opts?.dpopAlgs ?? [...ISSUER_DPOP_ALGS_DEFAULT], - credentialConfigurationsSupported: credentialConfigurations.credentialConfigurationsSupported ?? [], + credentialConfigurationsSupported: credentialConfigurations.credentialConfigurationsSupported ?? {}, batchCredentialIssuance: { batchSize: oidcIssuer?.batchCredentialIssuanceSize ?? batchCredentialIssuanceDefault } diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 245ff5f7c..1dd4b8584 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -200,7 +200,7 @@ export class Oid4vcIssuanceService { async oidcIssuerGetById(id: string, orgId: string): Promise { try { const getIssuerDetails = await this.oid4vcIssuanceRepository.getOidcIssuerDetailsById(id); - if (!getIssuerDetails && getIssuerDetails.publicIssuerId) { + if (!getIssuerDetails && !getIssuerDetails.publicIssuerId) { throw new NotFoundException(ResponseMessages.oidcIssuer.error.notFound); } const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); @@ -533,6 +533,9 @@ export class Oid4vcIssuanceService { } const agentDetails = await this.oid4vcIssuanceRepository.getAgentEndPoint(orgId); + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } //TDOD: signerOption should be under credentials change this with x509 support //TDOD: signerOption should be under credentials change this with x509 support diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index de17c4dc9..a08ed7208 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -283,7 +283,7 @@ export class Oid4vpVerificationService extends BaseService { } } - async getVerifierSession(orgId: string, query?: VerificationSessionQuery): Promise { + async getVerifierSession(orgId: string, query: VerificationSessionQuery): Promise { this.logger.debug(`[getVerifierSession] called for orgId=${orgId}, potentially with a query`); try { const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); diff --git a/apps/oid4vc-verification/test/app.e2e-spec.ts b/apps/oid4vc-verification/test/app.e2e-spec.ts index 4bb294287..a011c0ceb 100644 --- a/apps/oid4vc-verification/test/app.e2e-spec.ts +++ b/apps/oid4vc-verification/test/app.e2e-spec.ts @@ -1,6 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; import { Oid4vcVerificationModule } from './../src/oid4vc-verification.module'; describe('Oid4vcVerificationController (e2e)', () => { @@ -14,6 +13,4 @@ describe('Oid4vcVerificationController (e2e)', () => { app = moduleFixture.createNestApplication(); await app.init(); }); - - it('/ (GET)', () => request(app.getHttpServer()).get('/').expect(200).expect('Hello World!')); }); diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 2afeb5355..b75426178 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -120,7 +120,7 @@ export enum CommonConstants { // OID4VC URLs URL_OIDC_ISSUER_CREATE = '/openid4vc/issuer', /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase, @typescript-eslint/no-duplicate-enum-values */ - URL_OIDC_GET_ISSUES = '/openid4vc/issuer', + URL_OIDC_GET_ISSUERS = '/openid4vc/issuer', URL_OIDC_ISSUER_UPDATE = '/openid4vc/issuer/#', URL_OIDC_ISSUER_SESSIONS_CREATE = '/openid4vc/issuance-sessions/create-credential-offer', URL_OIDC_ISSUER_SESSIONS_GET = '/openid4vc/issuance-sessions/#', diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index 3642cf478..204cd3df4 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -94,7 +94,7 @@ export const getAgentUrl = (agentEndPoint: string, urlFlag: string, paramId?: st [String(CommonConstants.SEND_QUESTION), String(CommonConstants.URL_SEND_QUESTION)], [String(CommonConstants.SEND_BASIC_MESSAGE), String(CommonConstants.URL_SEND_BASIC_MESSAGE)], [String(CommonConstants.OIDC_ISSUER_CREATE), String(CommonConstants.URL_OIDC_ISSUER_CREATE)], - [String(CommonConstants.OIDC_GET_ALL_ISSUERS), String(CommonConstants.URL_OIDC_GET_ISSUES)], + [String(CommonConstants.OIDC_GET_ALL_ISSUERS), String(CommonConstants.URL_OIDC_GET_ISSUERS)], [String(CommonConstants.OIDC_ISSUER_DELETE), String(CommonConstants.URL_OIDC_ISSUER_UPDATE)], [String(CommonConstants.OIDC_ISSUER_BY_ID), String(CommonConstants.URL_OIDC_ISSUER_UPDATE)], [String(CommonConstants.OIDC_ISSUER_TEMPLATE), String(CommonConstants.URL_OIDC_ISSUER_UPDATE)], diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 54e259a3c..4ffe4dac5 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -571,10 +571,7 @@ export const ResponseMessages = { createFailed: 'Failed to create OID4VP verifier.', updateFailed: 'Failed to update OID4VP verifier.', deleteFailed: 'Failed to delete OID4VP verifier.', - notFoundIssuerDisplay: 'Issuer display not found.', - notFoundIssuerDetails: 'Issuer details not found.', - verifierIdAlreadyExists: 'Verifier ID already exists for this verifier.', - deleteTemplate: 'Error while deleting template.' + verifierIdAlreadyExists: 'Verifier ID already exists for this verifier.' } }, oid4vpSession: { @@ -592,8 +589,6 @@ export const ResponseMessages = { createFailed: 'Failed to create OID4VP session verifier.', updateFailed: 'Failed to update OID4VP session verifier.', deleteFailed: 'Failed to delete OID4VP session verifier.', - notFoundIssuerDisplay: 'Issuer display not found.', - notFoundIssuerDetails: 'Issuer details not found.', verifierIdAlreadyExists: 'Verifier ID already exists for this verifier.', deleteTemplate: 'Error while deleting template.' } @@ -610,7 +605,7 @@ export const ResponseMessages = { error: { errorCreate: 'Error while creating x509 certificate.', errorUpdateStatus: 'Error while updating x509 certificate.', - errorActivation: 'Failed to activate x509 certificate..', + errorActivation: 'Failed to activate x509 certificate.', agentEndPointNotFound: 'Agent details not found', collision: 'Certificate date range collides with existing certificates for this organization', collisionForActivatingX5c: diff --git a/libs/logger/src/logging.interceptor.ts b/libs/logger/src/logging.interceptor.ts index 953528e2d..301cca080 100644 --- a/libs/logger/src/logging.interceptor.ts +++ b/libs/logger/src/logging.interceptor.ts @@ -41,7 +41,7 @@ export class LoggingInterceptor implements NestInterceptor { }); } - private setupContextId(contextIdFromHeader: string | undefined): void { + private setupContextId(contextIdFromHeader: string): void { this.contextStorageService.set('x-correlation-id', contextIdFromHeader); this.contextStorageService.set('contextId', contextIdFromHeader); this.contextStorageService.setContextId(contextIdFromHeader); From e9477f1d5d73bcdeebe1f2478a54d9fb1849b5a5 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Fri, 28 Nov 2025 17:08:07 +0530 Subject: [PATCH 42/43] fix: changed error message Signed-off-by: Tipu_Singh --- apps/oid4vc-verification/src/oid4vc-verification.service.ts | 2 +- libs/common/src/response-messages/index.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index a08ed7208..0aa2d631b 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -330,7 +330,7 @@ export class Oid4vpVerificationService extends BaseService { ); const verifiers = await await this._getOid4vpVerifierSession(url, orgId); if (!verifiers || 0 === verifiers.length) { - throw new NotFoundException(ResponseMessages.oid4vp.error.notFound); + throw new NotFoundException(ResponseMessages.oid4vpSession.error.responseNotFound); } this.logger.debug(`[getVerificationSessionResponse] response fetched successfully for orgId=${orgId}`); return verifiers; diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 4ffe4dac5..83e72d546 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -590,7 +590,8 @@ export const ResponseMessages = { updateFailed: 'Failed to update OID4VP session verifier.', deleteFailed: 'Failed to delete OID4VP session verifier.', verifierIdAlreadyExists: 'Verifier ID already exists for this verifier.', - deleteTemplate: 'Error while deleting template.' + deleteTemplate: 'Error while deleting template.', + responseNotFound: 'Verification session response not found.' } }, x509: { From 691aee9a2e5a56561abb6637bce3d023bae4bba3 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Thu, 4 Dec 2025 11:59:22 +0530 Subject: [PATCH 43/43] feat: dcapi support (#1530) * feat: dcapi support Signed-off-by: Tipu_Singh * feat: added id validation in create presentation Signed-off-by: Tipu_Singh --------- Signed-off-by: Tipu_Singh --- .../dtos/oid4vc-verifier-presentation.dto.ts | 19 ++++++++++++++----- libs/enum/src/enum.ts | 7 +++++++ 2 files changed, 21 insertions(+), 5 deletions(-) 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 7d6113d09..ac8dbf415 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 @@ -11,10 +11,12 @@ import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, - Validate + Validate, + Matches } from 'class-validator'; import { Type } from 'class-transformer'; import { SignerOption } from '@prisma/client'; +import { ResponseMode } from '@credebl/enum/enum'; /** * DTO for verification-presentation query parameters. @@ -215,6 +217,9 @@ export class DcqlCredentialDto { @ApiProperty({ example: 'birthcertificate-dc_sd_jwt' }) @IsDefined() @IsString() + @Matches(/^[a-zA-Z0-9_-]+$/, { + message: 'id must only contain alphanumeric characters, underscores, and hyphens (dots are not allowed)' + }) id: string; @ApiProperty({ example: 'dc+sd-jwt' }) @@ -326,12 +331,16 @@ export class PresentationRequestDto { dcql?: DcqlDto; @ApiProperty({ - example: 'direct_post.jwt', - description: 'Response mode for the verifier' + example: ResponseMode.DIRECT_POST_JWT, + description: 'Response mode for the verifier', + enum: ResponseMode }) @IsDefined() - @IsString() - responseMode: string; + @IsEnum(ResponseMode) + responseMode: ResponseMode; + //TODO: check e2e flow and add ResponseMode based restrictions + @IsOptional() + expectedOrigins: string[]; /** * Dummy property used to run a class-level validation ensuring mutual exclusivity. diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 2c9829deb..200f37674 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -342,3 +342,10 @@ export declare enum HandshakeProtocol { Connections = 'https://didcomm.org/connections/1.0', DidExchange = 'https://didcomm.org/didexchange/1.0' } + +export enum ResponseMode { + DIRECT_POST = 'direct_post', + DIRECT_POST_JWT = 'direct_post.jwt', + DC_API = 'dc_api', + DC_API_JWT = 'dc_api.jwt' +}