import {
ApiExtraModels,
ApiHideProperty,
ApiProperty,
getSchemaPath,
} from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsArray,
IsBoolean,
IsEnum,
IsNumber,
IsObject,
IsOptional,
IsString,
ValidateNested,
} from "class-validator";
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
import { TenantEntity } from "../../../../auth/tenant/entitites/tenant.entity";
import { CertEntity } from "../../../../crypto/key/entities/cert.entity";
import { WebhookConfig } from "../../../../shared/utils/webhook/webhook.dto";
import { SchemaResponse } from "../../../issuance/oid4vci/metadata/dto/schema-response.dto";
import { VCT } from "../../../issuance/oid4vci/metadata/dto/vct.dto";
import {
IaeAction,
IaeActionBase,
IaeActionOpenid4vpPresentation,
IaeActionRedirectToWeb,
IaeActionType,
} from "./iae-action.dto";
import {
AllowListPolicy,
AttestationBasedPolicy,
EmbeddedDisclosurePolicy,
NoneTrustPolicy,
RootOfTrustPolicy,
} from "./policies.dto";
export class DisplayImage {
@IsString()
uri!: string;
}
export class Display {
@IsString()
name!: string;
@IsString()
description!: string;
@IsString()
locale!: string;
@IsOptional()
@IsString()
background_color?: string;
@IsOptional()
@IsString()
text_color?: string;
@IsOptional()
@ValidateNested()
@Type(() => DisplayImage)
background_image?: DisplayImage;
@IsOptional()
@ValidateNested()
@Type(() => DisplayImage)
logo?: DisplayImage;
}
export enum CredentialFormat {
MSO_MDOC = "mso_mdoc",
SD_JWT = "dc+sd-jwt",
}
export class IssuerMetadataCredentialConfig {
@IsEnum(CredentialFormat)
format!: CredentialFormat;
@ValidateNested()
@Type(() => Display)
display!: Display[];
@IsOptional()
@IsString()
scope?: string;
/**
* Document type for mDOC credentials (e.g., "org.iso.18013.5.1.mDL").
* Only applicable when format is "mso_mdoc".
*/
@IsOptional()
@IsString()
docType?: string;
/**
* Namespace for mDOC credentials (e.g., "org.iso.18013.5.1").
* Only applicable when format is "mso_mdoc".
* Used when claims are provided as a flat object.
*/
@IsOptional()
@IsString()
namespace?: string;
/**
* Claims organized by namespace for mDOC credentials.
* Allows specifying claims across multiple namespaces.
* Only applicable when format is "mso_mdoc".
* Example:
* {
* "org.iso.18013.5.1": { "given_name": "John", "family_name": "Doe" },
* "org.iso.18013.5.1.aamva": { "DHS_compliance": "F" }
* }
*/
@IsOptional()
@IsObject()
claimsByNamespace?: Record<string, Record<string, any>>;
}
@ApiExtraModels(
AttestationBasedPolicy,
NoneTrustPolicy,
AllowListPolicy,
RootOfTrustPolicy,
VCT,
IaeActionOpenid4vpPresentation,
IaeActionRedirectToWeb,
)
@Entity()
export class CredentialConfig {
@IsString()
@Column("varchar", { primary: true })
id!: string;
@IsString()
@Column("varchar", { nullable: true })
description?: string | null;
@ApiHideProperty()
@Column("varchar", { primary: true })
tenantId!: string;
/**
* The tenant that owns this object.
*/
@ManyToOne(() => TenantEntity, { cascade: true, onDelete: "CASCADE" })
tenant!: TenantEntity;
@Column("json")
@ValidateNested()
@Type(() => IssuerMetadataCredentialConfig)
config!: IssuerMetadataCredentialConfig;
@Column("json", { nullable: true })
@IsOptional()
@IsObject()
claims?: Record<string, any> | null;
/**
* Webhook to receive claims for the issuance process.
*/
@IsOptional()
@ValidateNested()
@Type(() => WebhookConfig)
@Column("json", { nullable: true })
claimsWebhook?: WebhookConfig | null;
/**
* Webhook to receive claims for the issuance process.
*/
@IsOptional()
@ValidateNested()
@Type(() => WebhookConfig)
@Column("json", { nullable: true })
notificationWebhook?: WebhookConfig | null;
// has to be optional since there may be credentials that are disclosed without a frame
@Column("json", { nullable: true })
@IsOptional()
@IsObject()
disclosureFrame?: Record<string, any> | null;
@IsOptional()
@ApiProperty({
description:
"VCT as a URI string (e.g., urn:eudi:pid:de:1) or as an object for EUDIPLO-hosted VCT",
nullable: true,
oneOf: [
{ type: "string", description: "VCT URI string" },
{ $ref: getSchemaPath(VCT) },
],
})
@Column("json", { nullable: true })
vct?: string | VCT | null;
@IsOptional()
@Column("boolean", { default: false })
@IsBoolean()
keyBinding?: boolean;
/**
* Reference to the certificate used for signing.
* Note: No DB-level FK constraint because CertEntity has a composite PK
* (id + tenantId) and SET NULL behavior cannot work when tenantId is
* part of this entity's own PK.
*/
@IsOptional()
@IsString()
@Column("varchar", { nullable: true })
certId?: string;
@ManyToOne(() => CertEntity, { createForeignKeyConstraints: false })
@JoinColumn([
{ name: "certId", referencedColumnName: "id" },
{ name: "tenantId", referencedColumnName: "tenantId" },
])
cert?: CertEntity;
@IsOptional()
@Column("boolean", { default: false })
@IsBoolean()
statusManagement?: boolean;
/**
* List of Interactive Authorization Endpoint (IAE) actions to execute
* before credential issuance. Actions are executed in order.
*
* Each action can be:
* - `openid4vp_presentation`: Request a verifiable presentation from the wallet
* - `redirect_to_web`: Redirect user to a web page for additional interaction
*
* If empty or not set, no interactive authorization is required.
*
* @example
* [
* { "type": "openid4vp_presentation", "presentationConfigId": "pid-config" },
* { "type": "redirect_to_web", "url": "https://example.com/verify", "label": "Additional Verification" }
* ]
*/
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => IaeActionBase, {
discriminator: {
property: "type",
subTypes: [
{
name: IaeActionType.OPENID4VP_PRESENTATION,
value: IaeActionOpenid4vpPresentation,
},
{
name: IaeActionType.REDIRECT_TO_WEB,
value: IaeActionRedirectToWeb,
},
],
},
keepDiscriminatorProperty: true,
})
@ApiProperty({
description:
"List of IAE actions to execute before credential issuance",
type: "array",
items: {
oneOf: [
{ $ref: getSchemaPath(IaeActionOpenid4vpPresentation) },
{ $ref: getSchemaPath(IaeActionRedirectToWeb) },
],
},
nullable: true,
required: false,
})
@Column("json", { nullable: true })
iaeActions?: IaeAction[] | null;
@IsOptional()
@Column("int", { nullable: true })
@IsNumber()
lifeTime?: number;
@IsOptional()
@ValidateNested()
@Type(() => SchemaResponse)
@Column("json", { nullable: true })
schema?: SchemaResponse | null;
/**
* Embedded disclosure policy (discriminated union by `policy`).
* The discriminator makes class-transformer instantiate the right subclass,
* and then class-validator runs that subclass’s rules.
*/
@IsOptional()
@ValidateNested()
@ApiProperty({
oneOf: [
{ $ref: getSchemaPath(AttestationBasedPolicy) },
{ $ref: getSchemaPath(NoneTrustPolicy) },
{ $ref: getSchemaPath(AllowListPolicy) },
{ $ref: getSchemaPath(RootOfTrustPolicy) },
],
})
@Type(() => AttestationBasedPolicy, {
discriminator: {
property: "policy",
subTypes: [
{ name: "none", value: NoneTrustPolicy },
{ name: "allowList", value: AllowListPolicy },
{ name: "rootOfTrust", value: RootOfTrustPolicy },
{
name: "attestationBased",
value: AttestationBasedPolicy,
},
],
},
keepDiscriminatorProperty: true, // keep `policy` on the instance
})
@Column("json", { nullable: true })
embeddedDisclosurePolicy?: EmbeddedDisclosurePolicy | null;
}