|
| 1 | +/* eslint-disable @typescript-eslint/explicit-function-return-type */ |
| 2 | +/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ |
| 3 | + |
| 4 | +import { |
| 5 | + IsArray, |
| 6 | + IsEnum, |
| 7 | + IsNotEmpty, |
| 8 | + IsObject, |
| 9 | + IsOptional, |
| 10 | + IsString, |
| 11 | + Matches, |
| 12 | + ValidateNested, |
| 13 | + registerDecorator, |
| 14 | + ValidationOptions, |
| 15 | + IsInt, |
| 16 | + Min, |
| 17 | + IsIn, |
| 18 | + ArrayMinSize, |
| 19 | + IsUrl, |
| 20 | + ValidatorConstraint, |
| 21 | + ValidatorConstraintInterface, |
| 22 | + ValidationArguments, |
| 23 | + Validate |
| 24 | +} from 'class-validator'; |
| 25 | +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; |
| 26 | +import { Type } from 'class-transformer'; |
| 27 | + |
| 28 | +export enum CredentialFormat { |
| 29 | + SdJwtVc = 'vc+sd-jwt', |
| 30 | + Mdoc = 'mdoc' |
| 31 | +} |
| 32 | + |
| 33 | +export enum SignerMethodOption { |
| 34 | + DID = 'did', |
| 35 | + X5C = 'x5c' |
| 36 | +} |
| 37 | + |
| 38 | +/** ---------- custom validator: disclosureFrame ---------- */ |
| 39 | +function isDisclosureFrameValue(v: unknown): boolean { |
| 40 | + if ('boolean' === typeof v) { |
| 41 | + return true; |
| 42 | + } |
| 43 | + if (v && 'object' === typeof v && !Array.isArray(v)) { |
| 44 | + return Object.values(v as Record<string, unknown>).every((x) => 'boolean' === typeof x); |
| 45 | + } |
| 46 | + return false; |
| 47 | +} |
| 48 | + |
| 49 | +export function IsDisclosureFrame(options?: ValidationOptions) { |
| 50 | + return function (object: unknown, propertyName: string) { |
| 51 | + registerDecorator({ |
| 52 | + name: 'IsDisclosureFrame', |
| 53 | + target: (object as object).constructor, |
| 54 | + propertyName, |
| 55 | + options, |
| 56 | + validator: { |
| 57 | + validate(value: unknown) { |
| 58 | + if (value === undefined) { |
| 59 | + return true; |
| 60 | + } |
| 61 | + if (!value || 'object' !== typeof value || Array.isArray(value)) { |
| 62 | + return false; |
| 63 | + } |
| 64 | + return Object.values(value as Record<string, unknown>).every(isDisclosureFrameValue); |
| 65 | + }, |
| 66 | + defaultMessage() { |
| 67 | + return 'disclosureFrame must be a map of booleans or nested maps of booleans'; |
| 68 | + } |
| 69 | + } |
| 70 | + }); |
| 71 | + }; |
| 72 | +} |
| 73 | + |
| 74 | +/** ---------- payload DTOs ---------- */ |
| 75 | +export class CredentialPayloadDto { |
| 76 | + @ApiPropertyOptional() |
| 77 | + @IsOptional() |
| 78 | + @IsString() |
| 79 | + vct?: string; |
| 80 | + |
| 81 | + @ApiPropertyOptional({ example: 'Garry' }) |
| 82 | + @IsOptional() |
| 83 | + @IsString() |
| 84 | + full_name?: string; |
| 85 | + |
| 86 | + @ApiPropertyOptional({ example: '2000-01-01', description: 'YYYY-MM-DD' }) |
| 87 | + @IsOptional() |
| 88 | + @Matches(/^\d{4}-\d{2}-\d{2}$/, { message: 'birth_date must be YYYY-MM-DD' }) |
| 89 | + birth_date?: string; |
| 90 | + |
| 91 | + @ApiPropertyOptional({ example: 'Africa' }) |
| 92 | + @IsOptional() |
| 93 | + @IsString() |
| 94 | + birth_place?: string; |
| 95 | + |
| 96 | + @ApiPropertyOptional({ example: 'James Bear' }) |
| 97 | + @IsOptional() |
| 98 | + @IsString() |
| 99 | + parent_names?: string; |
| 100 | + |
| 101 | + [key: string]: unknown; |
| 102 | +} |
| 103 | + |
| 104 | +export class CredentialRequestDto { |
| 105 | + @ApiProperty({ example: '1b2d3c4e-...' }) |
| 106 | + @IsString() |
| 107 | + @IsNotEmpty() |
| 108 | + templateId!: string; |
| 109 | + |
| 110 | + @ApiProperty({ enum: CredentialFormat, example: CredentialFormat.SdJwtVc }) |
| 111 | + @IsEnum(CredentialFormat) |
| 112 | + format!: CredentialFormat; |
| 113 | + |
| 114 | + @ApiProperty({ |
| 115 | + type: CredentialPayloadDto, |
| 116 | + description: 'Credential payload (structure depends on the format)' |
| 117 | + }) |
| 118 | + @ValidateNested() |
| 119 | + @Type(() => CredentialPayloadDto) |
| 120 | + payload!: CredentialPayloadDto; |
| 121 | + |
| 122 | + @ApiPropertyOptional({ |
| 123 | + description: 'Selective disclosure frame (claim -> boolean or nested map).', |
| 124 | + example: { full_name: true, birth_date: true, birth_place: false, parent_names: false }, |
| 125 | + required: false |
| 126 | + }) |
| 127 | + @IsOptional() |
| 128 | + @IsObject() |
| 129 | + @IsDisclosureFrame() |
| 130 | + disclosureFrame?: Record<string, boolean | Record<string, boolean>>; |
| 131 | +} |
| 132 | + |
| 133 | +/** ---------- auth-config DTOs ---------- */ |
| 134 | +export class TxCodeDto { |
| 135 | + @ApiPropertyOptional({ example: 'test abc' }) |
| 136 | + @IsOptional() |
| 137 | + @IsString() |
| 138 | + description?: string; |
| 139 | + |
| 140 | + @ApiProperty({ example: 4 }) |
| 141 | + @IsInt() |
| 142 | + @Min(1) |
| 143 | + length!: number; |
| 144 | + |
| 145 | + @ApiProperty({ example: 'numeric', enum: ['numeric'] }) |
| 146 | + @IsString() |
| 147 | + @IsIn(['numeric']) |
| 148 | + input_mode!: 'numeric'; |
| 149 | +} |
| 150 | + |
| 151 | +export class PreAuthorizedCodeFlowConfigDto { |
| 152 | + @ApiProperty({ type: TxCodeDto }) |
| 153 | + @ValidateNested() |
| 154 | + @Type(() => TxCodeDto) |
| 155 | + txCode!: TxCodeDto; |
| 156 | + |
| 157 | + @ApiProperty({ |
| 158 | + example: 'http://localhost:4001/oid4vci/abc-gov', |
| 159 | + description: 'AS (Authorization Server) base URL' |
| 160 | + }) |
| 161 | + @IsUrl({ require_tld: false }) |
| 162 | + authorizationServerUrl!: string; |
| 163 | +} |
| 164 | + |
| 165 | +export class AuthorizationCodeFlowConfigDto { |
| 166 | + @ApiProperty({ |
| 167 | + example: 'https://id.credebl.ae:8443/realms/credebl', |
| 168 | + description: 'AS (Authorization Server) base URL' |
| 169 | + }) |
| 170 | + @IsUrl({ require_tld: false }) |
| 171 | + authorizationServerUrl!: string; |
| 172 | +} |
| 173 | + |
| 174 | +/** ---------- class-level constraint: EXACTLY ONE of the two configs ---------- */ |
| 175 | +@ValidatorConstraint({ name: 'ExactlyOneOf', async: false }) |
| 176 | +class ExactlyOneOfConstraint implements ValidatorConstraintInterface { |
| 177 | + validate(_: unknown, args: ValidationArguments) { |
| 178 | + const obj = args.object as Record<string, unknown>; |
| 179 | + const keys = (args.constraints ?? []) as string[]; |
| 180 | + const present = keys.filter((k) => obj[k] !== undefined && null !== obj[k]); |
| 181 | + return 1 === present.length; |
| 182 | + } |
| 183 | + defaultMessage(args: ValidationArguments) { |
| 184 | + const keys = (args.constraints ?? []) as string[]; |
| 185 | + return `Exactly one of [${keys.join(', ')}] must be provided (not both, not neither).`; |
| 186 | + } |
| 187 | +} |
| 188 | +function ExactlyOneOf(keys: string[], options?: ValidationOptions) { |
| 189 | + return Validate(ExactlyOneOfConstraint, keys, options); |
| 190 | +} |
| 191 | + |
| 192 | +/** ---------- root DTO (no authenticationType) ---------- */ |
| 193 | +export class CreateOidcCredentialOfferDto { |
| 194 | + @ApiProperty({ |
| 195 | + type: [CredentialRequestDto], |
| 196 | + description: 'At least one credential to be issued.' |
| 197 | + }) |
| 198 | + @IsArray() |
| 199 | + @ArrayMinSize(1) |
| 200 | + @ValidateNested({ each: true }) |
| 201 | + @Type(() => CredentialRequestDto) |
| 202 | + credentials!: CredentialRequestDto[]; |
| 203 | + |
| 204 | + // Each is optional individually; XOR rule below enforces exactly one present. |
| 205 | + @ApiPropertyOptional({ type: PreAuthorizedCodeFlowConfigDto }) |
| 206 | + @IsOptional() |
| 207 | + @ValidateNested() |
| 208 | + @Type(() => PreAuthorizedCodeFlowConfigDto) |
| 209 | + preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfigDto; |
| 210 | + |
| 211 | + @ApiPropertyOptional({ type: AuthorizationCodeFlowConfigDto }) |
| 212 | + @IsOptional() |
| 213 | + @ValidateNested() |
| 214 | + @Type(() => AuthorizationCodeFlowConfigDto) |
| 215 | + authorizationCodeFlowConfig?: AuthorizationCodeFlowConfigDto; |
| 216 | + |
| 217 | + issuerId?: string; |
| 218 | + |
| 219 | + // Host the class-level XOR validator on a dummy property |
| 220 | + @ExactlyOneOf(['preAuthorizedCodeFlowConfig', 'authorizationCodeFlowConfig'], { |
| 221 | + message: 'Provide exactly one of preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig.' |
| 222 | + }) |
| 223 | + private readonly _exactlyOne?: unknown; |
| 224 | +} |
0 commit comments