import { JWTPayload } from 'jose'; import { JsonCredential } from '../../shared/dto/jsonCredential.dto.js'; import { DCQLQuery } from '../../shared/dto/dcqlQuery.dto.js'; import joseWrapper from '../../shared/middleware/joseWrapper.js'; import { JsonPresentation } from '../../shared/dto/jsonPresentation.dto.js'; export interface VerifiablePresentation extends JWTPayload { vp?: { id: string; '@context': string[]; type: string[]; holder: string; verifiableCredential: string[] | JsonCredential[]; }; } interface ExtractorResult { valid: boolean; message?: string; verifiableCredential?: CredentialFormatTuple; verifiableCredentialDecoded?: any; } export interface PresentationResult { valid: boolean; message?: string; } export enum CredentialFormat { JWT = 'jwt_vc', JSON = 'ldp_vc', } export interface CredentialFormatTuple { format: CredentialFormat; verifiableCredential: string | object; } export interface VPTokenData { vpTokenIssuer?: string; verifiableCredentials?: CredentialFormatTuple[]; verifiableCredentialsDecoded?: any[]; credentialQueryIds?: string[]; /** For JWT VPs this is the decoded JWT payload; for JSON-LD VPs it is the raw object. */ decodedVerifiablePresentation?: | VerifiablePresentation | VerifiablePresentation[] | JsonPresentation | JsonPresentation[]; verifiablePresentation?: object | object[] | string | string[]; } export interface ExtractionResult { result: PresentationResult; vpTokenData?: VPTokenData; } export class VpTokenCredentialsExtractor { private vpTokenIssuer: string; private decodedVerifiablePresentation: | VerifiablePresentation | VerifiablePresentation[] | JsonPresentation | JsonPresentation[]; private verifiablePresentation: object | object[] | string | string[]; constructor( private vpToken: object | object[] | string | string[], private dcqlQuery?: DCQLQuery, ) {} public extract(): ExtractionResult { const verifiableCredentials: CredentialFormatTuple[] = []; const verifiableCredentialsDecoded = []; const credentialQueryIds: string[] = []; if (this.dcqlQuery) { this.dcqlQuery.credentials.forEach((credentialQuery) => { const vp = this.vpToken[credentialQuery.id]; const { verifiableCredentials: credentialsFromVp, verifiableCredentialsDecoded: decodedCredentialsFromVp, } = this.getCredentialsFromVp(vp); verifiableCredentials.push(...credentialsFromVp); verifiableCredentialsDecoded.push(...decodedCredentialsFromVp); credentialQueryIds.push(credentialQuery.id); }); return { result: { valid: true, }, vpTokenData: { vpTokenIssuer: this.vpTokenIssuer, verifiableCredentials: verifiableCredentials, verifiableCredentialsDecoded: verifiableCredentialsDecoded, credentialQueryIds, decodedVerifiablePresentation: this.decodedVerifiablePresentation, verifiablePresentation: this.verifiablePresentation, }, }; } } private setVpIssuer(vpTokenElement: any[] | object | string) { if (vpTokenElement.hasOwnProperty('holder')) { this.setVpTokenIssuer((vpTokenElement as { holder: string }).holder); return; } if (vpTokenElement.hasOwnProperty('iss')) { this.setVpTokenIssuer((vpTokenElement as { iss: string }).iss); return; } if ( vpTokenElement.hasOwnProperty('issuer') && typeof (vpTokenElement as { issuer: string }).issuer === 'object' ) { this.setVpTokenIssuer( (vpTokenElement as { issuer: { id: string } }).issuer.id, ); return; } if ( vpTokenElement.hasOwnProperty('issuer') && typeof (vpTokenElement as { issuer: string }).issuer === 'string' ) { this.setVpTokenIssuer((vpTokenElement as { issuer: string }).issuer); return; } } private setVpTokenIssuer(issuer: string) { if (!this.vpTokenIssuer) { this.vpTokenIssuer = issuer; } } private getCredentialsFromVp(vp: object | object[] | string | string[]) { let credentials = []; const verifiableCredentials: CredentialFormatTuple[] = []; const verifiableCredentialsDecoded = []; if ( typeof vp === 'string' || (Array.isArray(vp) && typeof vp[0] === 'string') ) { // ── Branch 1: JWT VP ──────────────────────────────────────────────── // The VP is a JWT string; decode the payload to read the embedded vp claim. const decodedVp: VerifiablePresentation = joseWrapper.decodeJWT( Array.isArray(vp) ? vp[0] : (vp as string), ); this.setVpIssuer(decodedVp); this.decodedVerifiablePresentation = this.decodedVerifiablePresentation ? [].concat(this.decodedVerifiablePresentation as any, decodedVp) : decodedVp; credentials = decodedVp.vp.verifiableCredential; } else { const jsonVp = Array.isArray(vp) ? vp[0] : (vp as JsonPresentation); if (jsonVp.proof?.jws) { // ── Branch 2: VCDM 1.0 JSON-LD VP (proof.jws is a JWT) ─────────── // Decode the JWS to retrieve the embedded vp payload. const decodedVp: VerifiablePresentation = joseWrapper.decodeJWT( jsonVp.proof.jws, ); this.setVpIssuer(decodedVp); this.decodedVerifiablePresentation = this.decodedVerifiablePresentation ? [].concat(this.decodedVerifiablePresentation as any, decodedVp) : decodedVp; credentials = decodedVp.vp.verifiableCredential; } else { // ── Branch 3: VCDM 2.0 JSON-LD VP (DataIntegrityProof / proofValue) // The VP document itself is the canonical representation; the proof // signature is NOT a JWT and must not be decoded as one. this.setVpIssuer(jsonVp); this.decodedVerifiablePresentation = this.decodedVerifiablePresentation ? [].concat(this.decodedVerifiablePresentation as any, jsonVp) : jsonVp; credentials = jsonVp.verifiableCredential; } } for (const credential of credentials) { let decodedCredential: any; let format = CredentialFormat.JSON; if ( typeof credential === 'string' && !credential.includes('~') && credential.startsWith('ey') ) { // JWT credential — decode the payload directly. format = CredentialFormat.JWT; decodedCredential = joseWrapper.decodeJWT(credential); } else if (typeof credential === 'object') { const jsonCredential = credential as JsonCredential; const proof = jsonCredential.proof; if (proof?.jws) { // VCDM 1.0 JSON-LD credential — decode the embedded JWS. decodedCredential = joseWrapper.decodeJWT(proof.jws); } else if (proof?.proofValue) { // VCDM 2.0 DataIntegrityProof credential — the credential object // IS the decoded representation; wrap it to match the shape expected // by downstream validators (decodedCredential.vc / decodedCredential.iss). const issuer = typeof jsonCredential.issuer === 'string' ? jsonCredential.issuer : jsonCredential.issuer?.id; decodedCredential = { vc: jsonCredential, iss: issuer }; } } verifiableCredentials.push({ format, verifiableCredential: credential }); verifiableCredentialsDecoded.push(decodedCredential); } return { verifiableCredentials, verifiableCredentialsDecoded }; } }