From 4b27b9b96fa545348a2385767ef7324bae40ed8b Mon Sep 17 00:00:00 2001 From: mkuhn Date: Thu, 19 Mar 2026 13:56:25 +0100 Subject: [PATCH 01/31] feat: verify presentation token --- .../app-runtime/src/AppStringProcessor.ts | 10 ++- .../modules/openid4vc/OpenId4VcController.ts | 4 ++ .../src/modules/openid4vc/local/Holder.ts | 15 +++++ .../facades/consumption/OpenId4VcFacade.ts | 8 ++- .../runtime/src/useCases/common/Schemas.ts | 63 +++++++++++++++++++ .../openid4vc/VerifyPresentationToken.ts | 39 ++++++++++++ .../test/consumption/openid4vc.test.ts | 46 ++++++++++++-- 7 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index 1f494ae75..4246f5479 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -290,11 +290,15 @@ export class AppStringProcessor { // RelationshipTemplates are processed by the RequestModule break; case "Token": - const tokenContent = this.parseTokenContent(result.value.value.content); + const token = result.value.value; + const tokenContent = this.parseTokenContent(token.content); if (tokenContent instanceof TokenContentVerifiablePresentation) { - // TODO: add technical validation - await uiBridge.showVerifiablePresentation(account, result.value.value, true); + const verificationResult = await services.consumptionServices.openId4Vc.verifyPresentationToken({ + tokenContent: tokenContent.toJSON(), + expectedNonce: token.id + }); + await uiBridge.showVerifiablePresentation(account, result.value.value, verificationResult.value.isValid); break; } return Result.fail(AppRuntimeErrors.appStringProcessor.notSupportedTokenContent()); diff --git a/packages/consumption/src/modules/openid4vc/OpenId4VcController.ts b/packages/consumption/src/modules/openid4vc/OpenId4VcController.ts index 9d59f4e36..c7c6f11fa 100644 --- a/packages/consumption/src/modules/openid4vc/OpenId4VcController.ts +++ b/packages/consumption/src/modules/openid4vc/OpenId4VcController.ts @@ -133,4 +133,8 @@ export class OpenId4VcController extends ConsumptionBaseController { public async createPresentationTokenContent(credential: VerifiableCredential, nonce: string): Promise { return await this.holder.createPresentationTokenContent(credential, nonce); } + + public async verifyPresentationTokenContent(tokenContent: TokenContentVerifiablePresentation, expectedNonce: string): Promise<{ isValid: boolean; error?: Error }> { + return await this.holder.verifyPresentationTokenContent(tokenContent, expectedNonce); + } } diff --git a/packages/consumption/src/modules/openid4vc/local/Holder.ts b/packages/consumption/src/modules/openid4vc/local/Holder.ts index f3b4ea48d..7f98b3088 100644 --- a/packages/consumption/src/modules/openid4vc/local/Holder.ts +++ b/packages/consumption/src/modules/openid4vc/local/Holder.ts @@ -224,6 +224,21 @@ export class Holder extends BaseAgent> }); } + public async verifyPresentationTokenContent(tokenContent: TokenContentVerifiablePresentation, expectedNonce: string): Promise<{ isValid: boolean; error?: Error }> { + if (tokenContent.type !== ClaimFormat.SdJwtDc) throw new Error("Only SD-JWT credentials have been tested so far with token presentation"); + + const sdJwtVcApi = this.agent.dependencyManager.resolve(SdJwtVcApi); + const verificationResult = await sdJwtVcApi.verify({ + compactSdJwtVc: tokenContent.value as string, + keyBinding: { + audience: "defaultPresentationAudience", + nonce: expectedNonce + } + }); + + return { isValid: verificationResult.isValid, error: "error" in verificationResult ? verificationResult.error : undefined }; + } + public async exit(): Promise { await this.shutdown(); } diff --git a/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts b/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts index 00b65dc75..5e994619a 100644 --- a/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts +++ b/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts @@ -19,6 +19,7 @@ import { StoreCredentialsRequest, StoreCredentialsUseCase } from "../../../useCases"; +import { VerifyPresentationTokenRequest, VerifyPresentationTokenResponse, VerifyPresentationTokenUseCase } from "../../../useCases/consumption/openid4vc/VerifyPresentationToken"; export class OpenId4VcFacade { public constructor( @@ -27,7 +28,8 @@ export class OpenId4VcFacade { @Inject private readonly storeCredentialsUseCase: StoreCredentialsUseCase, @Inject private readonly resolveAuthorizationRequestUseCase: ResolveAuthorizationRequestUseCase, @Inject private readonly acceptAuthorizationRequestUseCase: AcceptAuthorizationRequestUseCase, - @Inject private readonly createPresentationTokenUseCase: CreatePresentationTokenUseCase + @Inject private readonly createPresentationTokenUseCase: CreatePresentationTokenUseCase, + @Inject private readonly verifyPresentationTokenUseCase: VerifyPresentationTokenUseCase ) {} public async resolveCredentialOffer(request: ResolveCredentialOfferRequest): Promise> { @@ -53,4 +55,8 @@ export class OpenId4VcFacade { public async createPresentationToken(request: CreatePresentationTokenRequest): Promise> { return await this.createPresentationTokenUseCase.execute(request); } + + public async verifyPresentationToken(request: VerifyPresentationTokenRequest): Promise> { + return await this.verifyPresentationTokenUseCase.execute(request); + } } diff --git a/packages/runtime/src/useCases/common/Schemas.ts b/packages/runtime/src/useCases/common/Schemas.ts index 1c8a4a434..663b11f55 100644 --- a/packages/runtime/src/useCases/common/Schemas.ts +++ b/packages/runtime/src/useCases/common/Schemas.ts @@ -20712,6 +20712,69 @@ export const LoadPeerTokenRequest: any = { } } +export const VerifyPresentationTokenRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/VerifyPresentationTokenRequest", + "definitions": { + "VerifyPresentationTokenRequest": { + "type": "object", + "properties": { + "tokenContent": { + "$ref": "#/definitions/TokenContentVerifiablePresentationJSON" + }, + "expectedNonce": { + "type": "string" + } + }, + "required": [ + "tokenContent", + "expectedNonce" + ], + "additionalProperties": false + }, + "TokenContentVerifiablePresentationJSON": { + "type": "object", + "properties": { + "@type": { + "type": "string", + "const": "TokenContentVerifiablePresentation" + }, + "@context": { + "type": "string" + }, + "@version": { + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object" + } + ] + }, + "type": { + "type": "string" + }, + "displayInformation": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "required": [ + "@type", + "type", + "value" + ], + "additionalProperties": false + } + } +} + export const CommunicationLanguage: any = { "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/CommunicationLanguage", diff --git a/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts b/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts new file mode 100644 index 000000000..b9094cee6 --- /dev/null +++ b/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts @@ -0,0 +1,39 @@ +import { Result } from "@js-soft/ts-utils"; +import { OpenId4VcController } from "@nmshd/consumption"; +import { TokenContentVerifiablePresentation, TokenContentVerifiablePresentationJSON } from "@nmshd/content"; +import { Inject } from "@nmshd/typescript-ioc"; +import { SchemaRepository, SchemaValidator, UseCase } from "../../common"; + +export interface VerifyPresentationTokenRequest { + tokenContent: TokenContentVerifiablePresentationJSON; + expectedNonce: string; +} + +export interface VerifyPresentationTokenResponse { + isValid: boolean; + error?: Error; +} + +class Validator extends SchemaValidator { + public constructor(@Inject schemaRepository: SchemaRepository) { + super(schemaRepository.getSchema("VerifyPresentationTokenRequest")); + } +} + +export class VerifyPresentationTokenUseCase extends UseCase { + public constructor( + @Inject private readonly openId4VcController: OpenId4VcController, + @Inject validator: Validator + ) { + super(validator); + } + + protected override async executeInternal(request: VerifyPresentationTokenRequest): Promise> { + const verificationResult = await this.openId4VcController.verifyPresentationTokenContent( + TokenContentVerifiablePresentation.from(request.tokenContent), + request.expectedNonce + ); + + return Result.ok(verificationResult); + } +} diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 2a9614fe8..3b958bf85 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -1,4 +1,4 @@ -import { SdJwtVcRecord } from "@credo-ts/core"; +import { ClaimFormat, SdJwtVcRecord } from "@credo-ts/core"; import { EudiploClient } from "@eudiplo/sdk-core"; import { AcceptProposeAttributeRequestItemParametersWithNewAttributeJSON, AcceptShareAuthorizationRequestRequestItemParametersJSON, decodeRecord } from "@nmshd/consumption"; import { RequestJSON, ShareAuthorizationRequestRequestItemJSON, TokenContentVerifiablePresentation, VerifiableCredentialJSON } from "@nmshd/content"; @@ -182,9 +182,7 @@ describe("EUDIPLO", () => { expect(presentationResult.value.status).toBe(200); }); - // TODO: unskip once fix to CanCreateShareCredentialOffer has been deployed to the connector - // eslint-disable-next-line jest/no-disabled-tests - test.skip("issuance with request", async () => { + test("issuance with request", async () => { const oldCredentials = ( await runtimeServices1.consumption.attributes.getAttributes({ query: { @@ -288,6 +286,44 @@ describe("EUDIPLO", () => { expect((presentationTokenContent as TokenContentVerifiablePresentation).value).toBeDefined(); expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation).toBeDefined(); expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation![0].name).toBe("test"); + + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + tokenContent: presentationTokenContent, + expectedNonce: presentationTokenResult.value.id + }); + + expect(verificationResult).toBeSuccessful(); + expect(verificationResult.value.isValid).toBe(true); + }); + + test.only("successful token verification", async () => { + // Problem: Credential zu kurzlebig + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + tokenContent: { + "@type": "TokenContentVerifiablePresentation", + type: ClaimFormat.SdJwtDc, + value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmdEQ0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBeU1UY3hNelU1TVRaYUZ3MHlOekF5TVRjeE16VTVNVFphTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05KQURCR0FpRUE3RTNia2VNZFF2bmRnQ2thenh1SzhjemxhRklJNmtoNVMyM3ZNOGFwa0h3Q0lRQzh6VDNqbHNlTHVHZjVNREZpbCtBbUM0MzV2eVBNQ2tBV3pkK3VBeXZJUmc9PSJdLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzM5MTgyNDksImV4cCI6MTc3MzkyMTg0OSwidmN0IjoidGVzdCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC90ZXN0IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Ik9URWttWHpiZXBFMThnVG8tMUxHcjE2bDVHNWdtbGtnSjZTM251N1ZSTWsiLCJ5IjoiejhHWkd3M1o3bVpHZ1oyZUE3LTMxVUdoZi1Sak5GVjc2Q2Rta1N4ZlAzQSIsImtpZCI6IjQ3MDFhNTQ2LTc5YmItNDJjMy05YTQ1LTQzNmIzYTU0MDMzYyJ9fSwiX3NkX2FsZyI6InNoYS0yNTYifQ.OYnl0hPesabuQ_XDuKFERwSeOFCtb9GHG0zaEuvAsFsH-srW9-A8npruZDpXlzhWsW85X-SNzNNsCBzJqBPfLA~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzM5MTgyNTMuMjYzLCJub25jZSI6IlRPS2pRSENOZjlvWFBCMGYzU05QIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6ImJaS042NkhhblBZWkRDaF91RjhiUHRUbjBuekwxeXlURGNLZjRXYWN6dEkifQ.PcIF1cmsi71-qNP23A6nnu-SL6D_QLalUsZ9dJKzI7H8mLF9TdN2-J94VNVfoTfs6ejL1F1_6C3z2kKKTe3PRA" + }, + expectedNonce: "TOKjQHCNf9oXPB0f3SNP" + }); + + expect(verificationResult).toBeSuccessful(); + console.log(verificationResult); + expect(verificationResult.value.isValid).toBe(true); + }); + + test("fail token verification in case of invalid signature", async () => { + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + tokenContent: { + "@type": "TokenContentVerifiablePresentation", + type: ClaimFormat.SdJwtDc, + value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmdEQ0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBeU1UY3hNelU1TVRaYUZ3MHlOekF5TVRjeE16VTVNVFphTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05KQURCR0FpRUE3RTNia2VNZFF2bmRnQ2thenh1SzhjemxhRklJNmtoNVMyM3ZNOGFwa0h3Q0lRQzh6VDNqbHNlTHVHZjVNREZpbCtBbUM0MzV2eVBNQ2tBV3pkK3VBeXZJUmc9PSJdLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzM5MTgyNDksImV4cCI6MTc3MzkyMTg0OSwidmN0IjoidGVzdCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC90ZXN0IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Ik9URWttWHpiZXBFMThnVG8tMUxHcjE2bDVHNWdtbGtnSjZTM251N1ZSTWsiLCJ5IjoiejhHWkd3M1o3bVpHZ1oyZUE3LTMxVUdoZi1Sak5GVjc2Q2Rta1N4ZlAzQSIsImtpZCI6IjQ3MDFhNTQ2LTc5YmItNDJjMy05YTQ1LTQzNmIzYTU0MDMzYyJ9fSwiX3NkX2FsZyI6InNoYS0yNTYifQ.OYnl0hPesabuQ_XDuKFERwSeOFCtb9GHG0zaEuvAsFsH-srW9-A8npruZDpXlzhWsW85X-SNzNNsCBzJqBPfL~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzM5MTgyNTMuMjYzLCJub25jZSI6IlRPS2pRSENOZjlvWFBCMGYzU05QIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6ImJaS042NkhhblBZWkRDaF91RjhiUHRUbjBuekwxeXlURGNLZjRXYWN6dEkifQ.PcIF1cmsi71-qNP23A6nnu-SL6D_QLalUsZ9dJKzI7H8mLF9TdN2-J94VNVfoTfs6ejL1F1_6C3z2kKKTe3PRA" + }, + expectedNonce: "TOKjQHCNf9oXPB0f3SNP" + }); + + expect(verificationResult).toBeSuccessful(); + expect(verificationResult.value.isValid).toBe(false); }); }); @@ -298,6 +334,8 @@ async function startOid4VcComposeStack() { if (baseUrl.includes("localhost")) { addressGenerationHostnameOverride = "localhost"; baseUrl = baseUrl.replace("localhost", "host.docker.internal"); + } else { + addressGenerationHostnameOverride = process.env.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE; } const composeFolder = path.resolve(path.join(__dirname, "..", "..", "..", "..", ".dev")); From 250237f10404ef9d60fdd1a888a3815ea9058d8c Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 20 Mar 2026 10:39:04 +0100 Subject: [PATCH 02/31] fix: allow empty tokens without password --- .../src/dtos/transport/EmptyTokenDTO.ts | 2 +- .../openid4vc/CreatePresentationToken.ts | 3 +- .../src/modules/tokens/TokenController.ts | 37 ++++++++++++------- .../src/modules/tokens/local/EmptyToken.ts | 8 ++-- .../tokens/local/SendEmptyTokenParameters.ts | 5 +++ .../local/UpdateTokenContentParameters.ts | 4 +- 6 files changed, 36 insertions(+), 23 deletions(-) diff --git a/packages/runtime-types/src/dtos/transport/EmptyTokenDTO.ts b/packages/runtime-types/src/dtos/transport/EmptyTokenDTO.ts index 162646e86..4d69ff58e 100644 --- a/packages/runtime-types/src/dtos/transport/EmptyTokenDTO.ts +++ b/packages/runtime-types/src/dtos/transport/EmptyTokenDTO.ts @@ -3,7 +3,7 @@ import { PasswordProtectionDTO } from "./PasswordProtectionDTO"; export interface EmptyTokenDTO { id: string; expiresAt: string; - passwordProtection: PasswordProtectionDTO; + passwordProtection?: PasswordProtectionDTO; reference: { truncated: string; url: string; diff --git a/packages/runtime/src/useCases/consumption/openid4vc/CreatePresentationToken.ts b/packages/runtime/src/useCases/consumption/openid4vc/CreatePresentationToken.ts index b203b4a5b..40bb9702b 100644 --- a/packages/runtime/src/useCases/consumption/openid4vc/CreatePresentationToken.ts +++ b/packages/runtime/src/useCases/consumption/openid4vc/CreatePresentationToken.ts @@ -41,8 +41,7 @@ export class CreatePresentationTokenUseCase extends UseCase { + if (!passwordDesired) return { hashedPassword: undefined, passwordProtection: undefined }; + + const password = await Random.string(16, RandomCharacterRange.Alphanumeric + RandomCharacterRange.SpecialCharacters); + const salt = await CoreCrypto.random(16); + const hashedPassword = (await CoreCrypto.deriveHashOutOfPassword(password, salt)).toBase64(); + const passwordProtection = PasswordProtection.from({ password, passwordType: "pw", salt }); + + return { hashedPassword, passwordProtection }; + } + @log() public async setTokenMetadata(idOrToken: CoreId | Token, metadata: ISerializable): Promise { const id = idOrToken instanceof CoreId ? idOrToken.toString() : idOrToken.id.toString(); @@ -233,19 +241,20 @@ export class TokenController extends TransportController { const cipher = await CoreCrypto.encrypt(serializedTokenBuffer, input.secretKey); - const password = parameters.passwordProtection.password; - if (!password) throw TransportCoreErrors.general.noPasswordProvided(); - - const hashedPassword = (await CoreCrypto.deriveHashOutOfPassword(password, input.passwordProtection.salt)).toBase64(); + const hashedPassword = input.passwordProtection?.password + ? (await CoreCrypto.deriveHashOutOfPassword(input.passwordProtection.password, input.passwordProtection.salt)).toBase64() + : undefined; const response = (await this.client.updateTokenContent({ id: parameters.id.toString(), newContent: cipher.toBase64(), password: hashedPassword })).value; - const passwordProtection = PasswordProtection.from({ - password, - passwordType: parameters.passwordProtection.passwordType, - salt: parameters.passwordProtection.salt, - passwordLocationIndicator: parameters.passwordProtection.passwordLocationIndicator - }); + const passwordProtection = input.passwordProtection?.password + ? PasswordProtection.from({ + password: input.passwordProtection.password, + passwordType: input.passwordProtection.passwordType, + salt: input.passwordProtection.salt, + passwordLocationIndicator: input.passwordProtection.passwordLocationIndicator + }) + : undefined; const token = Token.from({ id: CoreId.from(response.id), diff --git a/packages/transport/src/modules/tokens/local/EmptyToken.ts b/packages/transport/src/modules/tokens/local/EmptyToken.ts index 0a3616da5..f746d6b3c 100644 --- a/packages/transport/src/modules/tokens/local/EmptyToken.ts +++ b/packages/transport/src/modules/tokens/local/EmptyToken.ts @@ -9,7 +9,7 @@ import { TokenReference } from "../transmission/TokenReference"; export interface IEmptyToken extends ICoreSynchronizable { secretKey: ICryptoSecretKey; expiresAt: ICoreDate; - passwordProtection: IPasswordProtection; + passwordProtection?: IPasswordProtection; } @type("EmptyToken") @@ -25,9 +25,9 @@ export class EmptyToken extends CoreSynchronizable implements IEmptyToken { @serialize() public expiresAt: CoreDate; - @validate() + @validate({ nullable: true }) @serialize() - public passwordProtection: PasswordProtection; + public passwordProtection?: PasswordProtection; public static from(value: IEmptyToken): EmptyToken { return this.fromAny(value); @@ -38,7 +38,7 @@ export class EmptyToken extends CoreSynchronizable implements IEmptyToken { id: this.id, backboneBaseUrl, key: this.secretKey, - passwordProtection: this.passwordProtection.toSharedPasswordProtection(true) + passwordProtection: this.passwordProtection?.toSharedPasswordProtection(true) }); } } diff --git a/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts b/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts index f09cea641..6b17eb70e 100644 --- a/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts +++ b/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts @@ -4,6 +4,7 @@ import { CoreDate, ICoreDate } from "@nmshd/core-types"; export interface ISendEmptyTokenParameters extends ISerializable { expiresAt: ICoreDate; ephemeral: boolean; + withPassword?: boolean; } @type("SendEmptyTokenParameters") @@ -16,6 +17,10 @@ export class SendEmptyTokenParameters extends Serializable implements ISendEmpty @serialize() public ephemeral: boolean; + @validate({ nullable: true }) + @serialize() + public withPassword?: boolean; + public static from(value: ISendEmptyTokenParameters): SendEmptyTokenParameters { return this.fromAny(value); } diff --git a/packages/transport/src/modules/tokens/local/UpdateTokenContentParameters.ts b/packages/transport/src/modules/tokens/local/UpdateTokenContentParameters.ts index 8a4551501..f587ba2bb 100644 --- a/packages/transport/src/modules/tokens/local/UpdateTokenContentParameters.ts +++ b/packages/transport/src/modules/tokens/local/UpdateTokenContentParameters.ts @@ -6,7 +6,7 @@ export interface IUpdateTokenContentParameters extends ISerializable { id: ICoreId; secretKey: ICryptoSecretKey; content: ISerializable; - passwordProtection: ISharedPasswordProtection; + passwordProtection?: ISharedPasswordProtection; } @type("UpdateTokenContentParameters") @@ -25,7 +25,7 @@ export class UpdateTokenContentParameters extends Serializable implements IUpdat @validate({ nullable: true }) @serialize() - public passwordProtection: SharedPasswordProtection; + public passwordProtection?: SharedPasswordProtection; public static from(value: IUpdateTokenContentParameters): UpdateTokenContentParameters { return this.fromAny(value); From d4b4f58ba3602745e10145de9b0f981038842b95 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 20 Mar 2026 11:29:32 +0100 Subject: [PATCH 03/31] test: adapt app string processor test --- .../test/runtime/AppStringProcessor.test.ts | 69 +++++++++++++++++-- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 8396618dd..84315e5ea 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -1,7 +1,10 @@ -import { ArbitraryRelationshipTemplateContentJSON, AuthenticationRequestItem, RelationshipTemplateContent, TokenContentVerifiablePresentation } from "@nmshd/content"; +import { EudiploClient } from "@eudiplo/sdk-core"; +import { ArbitraryRelationshipTemplateContentJSON, AuthenticationRequestItem, RelationshipTemplateContent } from "@nmshd/content"; import { CoreDate, PasswordLocationIndicatorOptions } from "@nmshd/core-types"; import { DeviceOnboardingInfoDTO, PeerRelationshipTemplateLoadedEvent } from "@nmshd/runtime"; import assert from "assert"; +import path from "path"; +import { GenericContainer, Wait } from "testcontainers"; import { AppRuntime, LocalAccountSession } from "../../src"; import { MockEventBus, MockUIBridge, TestUtil } from "../lib"; @@ -378,11 +381,40 @@ describe("AppStringProcessor", function () { }); test("get a token with verifiable presentation content using a url", async function () { - const tokenResult = await runtime1Session.transportServices.tokens.createOwnToken({ - content: TokenContentVerifiablePresentation.from({ - value: { claim: "test" }, - type: "dc+sd-jwt" - }).toJSON(), + const eudiploClientId = "test-admin"; + const eudiploClientSecret = "57c9cd444bf402b2cc1f5a0d2dafd3955bd9042c0372db17a4ede2d5fbda88e5"; + const eudiploCredentialConfigurationId = "test"; + const eudiploBaseUrl = `http://localhost:3000`; + + const eudiploContainer = await startEudiplo(); + + const eudiploClient = new EudiploClient({ + baseUrl: eudiploBaseUrl, + clientId: eudiploClientId, + clientSecret: eudiploClientSecret + }); + + const credentialOfferUrl = ( + await eudiploClient.createIssuanceOffer({ + responseType: "uri", + credentialConfigurationIds: [eudiploCredentialConfigurationId], + flow: "pre_authorized_code" + }) + ).uri; + + const resolveCredentialOfferResult = await runtime1Session.consumptionServices.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); + const credentialResponsesResult = await runtime1Session.consumptionServices.openId4Vc.requestCredentials({ + credentialOffer: resolveCredentialOfferResult.value.credentialOffer, + credentialConfigurationIds: [eudiploCredentialConfigurationId] + }); + const storedCredential = ( + await runtime1Session.consumptionServices.openId4Vc.storeCredentials({ + credentialResponses: credentialResponsesResult.value.credentialResponses + }) + ).value; + + const tokenResult = await runtime1Session.consumptionServices.openId4Vc.createPresentationToken({ + attributeId: storedCredential.id, expiresAt: CoreDate.utc().add({ days: 1 }).toISOString(), ephemeral: true }); @@ -393,6 +425,8 @@ describe("AppStringProcessor", function () { expect(result.value).toBeUndefined(); expect(runtime4MockUiBridge).showVerifiablePresentationCalled(token.id, true); + + await eudiploContainer.stop(); }); test("get a template using a url", async function () { @@ -481,4 +515,27 @@ describe("AppStringProcessor", function () { expect(runtime4MockUiBridge).showFileCalled(file.id); }); }); + + async function startEudiplo() { + return await new GenericContainer("ghcr.io/openwallet-foundation-labs/eudiplo:3.1.2@sha256:0ea3a73d42a1eb10a6edc45e3289478b08b09064bd75563c503ed12be2ed2dc6") + .withEnvironment({ + PUBLIC_URL: "http://localhost:3000", // eslint-disable-line @typescript-eslint/naming-convention + MASTER_SECRET: "OgwrDcgVQQ2yZwcFt7kPxQm3nUF+X3etF6MdLTstZAY=", // eslint-disable-line @typescript-eslint/naming-convention + AUTH_CLIENT_ID: "root", // eslint-disable-line @typescript-eslint/naming-convention + AUTH_CLIENT_SECRET: "test", // eslint-disable-line @typescript-eslint/naming-convention + CONFIG_IMPORT: "true", // eslint-disable-line @typescript-eslint/naming-convention + CONFIG_FOLDER: "/app/assets/config", // eslint-disable-line @typescript-eslint/naming-convention + PORT: "3000" // eslint-disable-line @typescript-eslint/naming-convention + } as Record) + .withExposedPorts({ container: 3000, host: 3000 }) + .withBindMounts([ + { + source: path.resolve(path.join(__dirname, "..", "..", "..", "..", ".dev", "eudiplo-assets")), + target: "/app/assets/config" + } + ]) + .withStartupTimeout(60000) + .withWaitStrategy(Wait.forHealthCheck()) + .start(); + } }); From 25e1098c89f538d986ce6899af81cb0f00159dae Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 20 Mar 2026 11:36:07 +0100 Subject: [PATCH 04/31] test: remove happy path isolation test --- .../runtime/test/consumption/openid4vc.test.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 3b958bf85..e7460cb36 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -296,22 +296,6 @@ describe("EUDIPLO", () => { expect(verificationResult.value.isValid).toBe(true); }); - test.only("successful token verification", async () => { - // Problem: Credential zu kurzlebig - const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ - tokenContent: { - "@type": "TokenContentVerifiablePresentation", - type: ClaimFormat.SdJwtDc, - value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmdEQ0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBeU1UY3hNelU1TVRaYUZ3MHlOekF5TVRjeE16VTVNVFphTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05KQURCR0FpRUE3RTNia2VNZFF2bmRnQ2thenh1SzhjemxhRklJNmtoNVMyM3ZNOGFwa0h3Q0lRQzh6VDNqbHNlTHVHZjVNREZpbCtBbUM0MzV2eVBNQ2tBV3pkK3VBeXZJUmc9PSJdLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzM5MTgyNDksImV4cCI6MTc3MzkyMTg0OSwidmN0IjoidGVzdCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC90ZXN0IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Ik9URWttWHpiZXBFMThnVG8tMUxHcjE2bDVHNWdtbGtnSjZTM251N1ZSTWsiLCJ5IjoiejhHWkd3M1o3bVpHZ1oyZUE3LTMxVUdoZi1Sak5GVjc2Q2Rta1N4ZlAzQSIsImtpZCI6IjQ3MDFhNTQ2LTc5YmItNDJjMy05YTQ1LTQzNmIzYTU0MDMzYyJ9fSwiX3NkX2FsZyI6InNoYS0yNTYifQ.OYnl0hPesabuQ_XDuKFERwSeOFCtb9GHG0zaEuvAsFsH-srW9-A8npruZDpXlzhWsW85X-SNzNNsCBzJqBPfLA~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzM5MTgyNTMuMjYzLCJub25jZSI6IlRPS2pRSENOZjlvWFBCMGYzU05QIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6ImJaS042NkhhblBZWkRDaF91RjhiUHRUbjBuekwxeXlURGNLZjRXYWN6dEkifQ.PcIF1cmsi71-qNP23A6nnu-SL6D_QLalUsZ9dJKzI7H8mLF9TdN2-J94VNVfoTfs6ejL1F1_6C3z2kKKTe3PRA" - }, - expectedNonce: "TOKjQHCNf9oXPB0f3SNP" - }); - - expect(verificationResult).toBeSuccessful(); - console.log(verificationResult); - expect(verificationResult.value.isValid).toBe(true); - }); - test("fail token verification in case of invalid signature", async () => { const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ tokenContent: { From 537ba8554d2cad9f13a0eef925558ad3cb7bb30f Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 20 Mar 2026 13:10:50 +0100 Subject: [PATCH 05/31] refactor: adapt to optional password protection of empty token --- .../FillDeviceOnboardingTokenWithNewDevice.ts | 5 +---- .../transport/src/modules/tokens/TokenController.ts | 12 ++++++------ .../modules/tokens/local/SendEmptyTokenParameters.ts | 4 ++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/runtime/src/useCases/transport/devices/FillDeviceOnboardingTokenWithNewDevice.ts b/packages/runtime/src/useCases/transport/devices/FillDeviceOnboardingTokenWithNewDevice.ts index 4ef390dac..53ac3fada 100644 --- a/packages/runtime/src/useCases/transport/devices/FillDeviceOnboardingTokenWithNewDevice.ts +++ b/packages/runtime/src/useCases/transport/devices/FillDeviceOnboardingTokenWithNewDevice.ts @@ -30,9 +30,6 @@ export class FillDeviceOnboardingTokenWithNewDeviceUseCase extends UseCase> { const reference = TokenReference.from(request.reference); - const passwordProtection = reference.passwordProtection; - if (!passwordProtection?.password) throw RuntimeErrors.devices.referenceNotPointingToAnEmptyToken(); - const isEmptyToken = await this.tokenController.isEmptyToken(reference); if (!isEmptyToken) throw RuntimeErrors.devices.referenceNotPointingToAnEmptyToken(); @@ -46,7 +43,7 @@ export class FillDeviceOnboardingTokenWithNewDeviceUseCase extends UseCase { - if (!passwordDesired) return { hashedPassword: undefined, passwordProtection: undefined }; + private async generatePasswordProtection(usePasswordProtection?: boolean): Promise<{ hashedPassword: string | undefined; passwordProtection: PasswordProtection | undefined }> { + if (!usePasswordProtection) return { hashedPassword: undefined, passwordProtection: undefined }; const password = await Random.string(16, RandomCharacterRange.Alphanumeric + RandomCharacterRange.SpecialCharacters); const salt = await CoreCrypto.random(16); @@ -272,9 +272,9 @@ export class TokenController extends TransportController { } public async isEmptyToken(reference: TokenReference): Promise { - if (!reference.passwordProtection?.password) throw TransportCoreErrors.general.noPasswordProvided(); - - const hashedPassword = (await CoreCrypto.deriveHashOutOfPassword(reference.passwordProtection.password, reference.passwordProtection.salt)).toBase64(); + const hashedPassword = reference.passwordProtection?.password + ? (await CoreCrypto.deriveHashOutOfPassword(reference.passwordProtection.password, reference.passwordProtection.salt)).toBase64() + : undefined; const response = (await this.client.getToken(reference.id.toString(), hashedPassword)).value; return !response.content; diff --git a/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts b/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts index 6b17eb70e..9fcea53f8 100644 --- a/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts +++ b/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts @@ -4,7 +4,7 @@ import { CoreDate, ICoreDate } from "@nmshd/core-types"; export interface ISendEmptyTokenParameters extends ISerializable { expiresAt: ICoreDate; ephemeral: boolean; - withPassword?: boolean; + usePasswordProtection?: boolean; } @type("SendEmptyTokenParameters") @@ -19,7 +19,7 @@ export class SendEmptyTokenParameters extends Serializable implements ISendEmpty @validate({ nullable: true }) @serialize() - public withPassword?: boolean; + public usePasswordProtection?: boolean; public static from(value: ISendEmptyTokenParameters): SendEmptyTokenParameters { return this.fromAny(value); From db2b579030c1268e5b08056a53136eefc17d1566 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 20 Mar 2026 13:18:51 +0100 Subject: [PATCH 06/31] refactor: export verify use case --- .../extensibility/facades/consumption/OpenId4VcFacade.ts | 6 ++++-- .../runtime/src/useCases/consumption/openid4vc/index.ts | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts b/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts index 5e994619a..ad16dbcfb 100644 --- a/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts +++ b/packages/runtime/src/extensibility/facades/consumption/OpenId4VcFacade.ts @@ -17,9 +17,11 @@ import { ResolveCredentialOfferResponse, ResolveCredentialOfferUseCase, StoreCredentialsRequest, - StoreCredentialsUseCase + StoreCredentialsUseCase, + VerifyPresentationTokenRequest, + VerifyPresentationTokenResponse, + VerifyPresentationTokenUseCase } from "../../../useCases"; -import { VerifyPresentationTokenRequest, VerifyPresentationTokenResponse, VerifyPresentationTokenUseCase } from "../../../useCases/consumption/openid4vc/VerifyPresentationToken"; export class OpenId4VcFacade { public constructor( diff --git a/packages/runtime/src/useCases/consumption/openid4vc/index.ts b/packages/runtime/src/useCases/consumption/openid4vc/index.ts index 024905d25..f7d23b806 100644 --- a/packages/runtime/src/useCases/consumption/openid4vc/index.ts +++ b/packages/runtime/src/useCases/consumption/openid4vc/index.ts @@ -4,3 +4,4 @@ export * from "./RequestCredentials"; export * from "./ResolveAuthorizationRequest"; export * from "./ResolveCredentialOffer"; export * from "./StoreCredentials"; +export * from "./VerifyPresentationToken"; From ff3a208840796580ba32dda01edceb4f095926a6 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 20 Mar 2026 13:26:03 +0100 Subject: [PATCH 07/31] test: cleaner file copying into container --- packages/app-runtime/test/runtime/AppStringProcessor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 84315e5ea..03977232e 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -528,7 +528,7 @@ describe("AppStringProcessor", function () { PORT: "3000" // eslint-disable-line @typescript-eslint/naming-convention } as Record) .withExposedPorts({ container: 3000, host: 3000 }) - .withBindMounts([ + .withCopyDirectoriesToContainer([ { source: path.resolve(path.join(__dirname, "..", "..", "..", "..", ".dev", "eudiplo-assets")), target: "/app/assets/config" From 45fc3cbc3e85ad3bbb315ee775f48e1b526b0934 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 20 Mar 2026 13:26:36 +0100 Subject: [PATCH 08/31] feat: improve unsupported VC type error message --- packages/consumption/src/modules/openid4vc/local/Holder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/consumption/src/modules/openid4vc/local/Holder.ts b/packages/consumption/src/modules/openid4vc/local/Holder.ts index 7f98b3088..701edd701 100644 --- a/packages/consumption/src/modules/openid4vc/local/Holder.ts +++ b/packages/consumption/src/modules/openid4vc/local/Holder.ts @@ -205,7 +205,7 @@ export class Holder extends BaseAgent> // hacky solution because credo doesn't support credentials without key binding // TODO: use credentials without key binding once supported public async createPresentationTokenContent(credential: VerifiableCredential, nonce: string): Promise { - if (credential.type !== ClaimFormat.SdJwtDc) throw new Error("Only SD-JWT credentials have been tested so far with token presentation"); + if (credential.type !== ClaimFormat.SdJwtDc) throw new Error("Only SD-JWT credentials are supported for token presentation"); const sdJwtVcApi = this.agent.dependencyManager.resolve(SdJwtVcApi); const presentation = await sdJwtVcApi.present({ @@ -225,7 +225,7 @@ export class Holder extends BaseAgent> } public async verifyPresentationTokenContent(tokenContent: TokenContentVerifiablePresentation, expectedNonce: string): Promise<{ isValid: boolean; error?: Error }> { - if (tokenContent.type !== ClaimFormat.SdJwtDc) throw new Error("Only SD-JWT credentials have been tested so far with token presentation"); + if (tokenContent.type !== ClaimFormat.SdJwtDc) throw new Error("Only SD-JWT credentials are supported for token presentation"); const sdJwtVcApi = this.agent.dependencyManager.resolve(SdJwtVcApi); const verificationResult = await sdJwtVcApi.verify({ From 6a61c3a5b99b8a4700e74a51036e8948b18ceb86 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 20 Mar 2026 13:58:06 +0100 Subject: [PATCH 09/31] test: add negative tests --- .../test/consumption/openid4vc.test.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index e7460cb36..d06a95392 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -252,7 +252,7 @@ describe("EUDIPLO", () => { expect(sessionStatus).toBe("completed"); // in case of failed presentation: Status remains "active" }); - test("create presentation token", async () => { + test("presentation with token", async () => { const credentialOfferUrl = ( await eudiploClient.createIssuanceOffer({ responseType: "uri", @@ -296,18 +296,34 @@ describe("EUDIPLO", () => { expect(verificationResult.value.isValid).toBe(true); }); + test("fail token verification in case of invalid nonce", async () => { + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + tokenContent: { + "@type": "TokenContentVerifiablePresentation", + type: ClaimFormat.SdJwtDc, + value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmZ6Q0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBek1EVXhNVEF4TlRGYUZ3MHlOekF6TURVeE1UQXhOVEZhTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05JQURCRkFpRUEwc1A3MWJXdzhYTTl0czZFT29JSE9Cb1V4V21EbHFVR1dESGp0SDU3S0cwQ0lGTU1sQUdHNElxcloxQ0pGMUJ1eWhWVTNaRWc0ck1acDNHdlgwNkt1UjhyIl0sImFsZyI6IkVTMjU2In0.eyJpYXQiOjE3NzQwMTAwNzgsImV4cCI6MTA0MTQwMTAwNzgsInZjdCI6InRlc3QiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiZlVKTEJZd1Bnb0RTOWRNWDh1TUVLYmpIVXNabGdHYTY2RUdpOEtoSDY1QSIsInkiOiJ4Q3Z1ZENfVDBsOVM5VnRGOGRXVHZrdkl2NUxDcmxWYlM1bDlZYTFQQVh3Iiwia2lkIjoiOTA1N2E5ZGMtMWRiMC00MWI3LWJmMDEtYmM3ODFkOThjMTA3In19LCJfc2RfYWxnIjoic2hhLTI1NiJ9.2u6b8GxPo2uvFHYHrDxfGnR25pgpmo1BZ2VEbb8pLJmPM_oJ9LmcrKpQut2GDi-tCkU6SvEQ7koG4VHPpFgYEA~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzQwMTAwODEuNDgzLCJub25jZSI6IlRPS3VFdWdTWWw2VFlEcmFmUngwIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6InRuVDdWNk9DUGVKeEkyN3JrVnRtcDBtMFAyaUxFMHpVSEN5dk8yNFNhVkUifQ.TloaQCqVBw-u6Zg7hfWnaMPB1od6YN3zqKokQF280SdF1d4H_h6IXvIVj7r89yROEMhl1eFf8zU02CHbkBNfpg" + }, + expectedNonce: "wrong-nonce" + }); + + expect(verificationResult).toBeSuccessful(); + expect(verificationResult.value.isValid).toBe(false); + expect(verificationResult.value.error?.message).toBe("Verify Error: Invalid Nonce"); + }); + test("fail token verification in case of invalid signature", async () => { const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ tokenContent: { "@type": "TokenContentVerifiablePresentation", type: ClaimFormat.SdJwtDc, - value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmdEQ0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBeU1UY3hNelU1TVRaYUZ3MHlOekF5TVRjeE16VTVNVFphTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05KQURCR0FpRUE3RTNia2VNZFF2bmRnQ2thenh1SzhjemxhRklJNmtoNVMyM3ZNOGFwa0h3Q0lRQzh6VDNqbHNlTHVHZjVNREZpbCtBbUM0MzV2eVBNQ2tBV3pkK3VBeXZJUmc9PSJdLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzM5MTgyNDksImV4cCI6MTc3MzkyMTg0OSwidmN0IjoidGVzdCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC90ZXN0IiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6Ik9URWttWHpiZXBFMThnVG8tMUxHcjE2bDVHNWdtbGtnSjZTM251N1ZSTWsiLCJ5IjoiejhHWkd3M1o3bVpHZ1oyZUE3LTMxVUdoZi1Sak5GVjc2Q2Rta1N4ZlAzQSIsImtpZCI6IjQ3MDFhNTQ2LTc5YmItNDJjMy05YTQ1LTQzNmIzYTU0MDMzYyJ9fSwiX3NkX2FsZyI6InNoYS0yNTYifQ.OYnl0hPesabuQ_XDuKFERwSeOFCtb9GHG0zaEuvAsFsH-srW9-A8npruZDpXlzhWsW85X-SNzNNsCBzJqBPfL~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzM5MTgyNTMuMjYzLCJub25jZSI6IlRPS2pRSENOZjlvWFBCMGYzU05QIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6ImJaS042NkhhblBZWkRDaF91RjhiUHRUbjBuekwxeXlURGNLZjRXYWN6dEkifQ.PcIF1cmsi71-qNP23A6nnu-SL6D_QLalUsZ9dJKzI7H8mLF9TdN2-J94VNVfoTfs6ejL1F1_6C3z2kKKTe3PRA" + value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmZ6Q0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBek1EVXhNVEF4TlRGYUZ3MHlOekF6TURVeE1UQXhOVEZhTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05JQURCRkFpRUEwc1A3MWJXdzhYTTl0czZFT29JSE9Cb1V4V21EbHFVR1dESGp0SDU3S0cwQ0lGTU1sQUdHNElxcloxQ0pGMUJ1eWhWVTNaRWc0ck1acDNHdlgwNkt1UjhyIl0sImFsZyI6IkVTMjU2In0.eyJpYXQiOjE3NzQwMTAwNzgsImV4cCI6MTA0MTQwMTAwNzgsInZjdCI6InRlc3QiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiZlVKTEJZd1Bnb0RTOWRNWDh1TUVLYmpIVXNabGdHYTY2RUdpOEtoSDY1QSIsInkiOiJ4Q3Z1ZENfVDBsOVM5VnRGOGRXVHZrdkl2NUxDcmxWYlM1bDlZYTFQQVh3Iiwia2lkIjoiOTA1N2E5ZGMtMWRiMC00MWI3LWJmMDEtYmM3ODFkOThjMTA3In19LCJfc2RfYWxnIjoic2hhLTI1NiJ9.2u6b8GxPo2uvFHYHrDxfGnR25pgpmo1BZ2VEbb8pLJmPM_oJ9LmcrKpQut2GDi-tCkU6SvEQ7koG4VHPpFgYE~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzQwMTAwODEuNDgzLCJub25jZSI6IlRPS3VFdWdTWWw2VFlEcmFmUngwIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6InRuVDdWNk9DUGVKeEkyN3JrVnRtcDBtMFAyaUxFMHpVSEN5dk8yNFNhVkUifQ.TloaQCqVBw-u6Zg7hfWnaMPB1od6YN3zqKokQF280SdF1d4H_h6IXvIVj7r89yROEMhl1eFf8zU02CHbkBNfpg" }, expectedNonce: "TOKjQHCNf9oXPB0f3SNP" }); expect(verificationResult).toBeSuccessful(); expect(verificationResult.value.isValid).toBe(false); + expect(verificationResult.value.error?.message).toBe("Verify Error: Invalid JWT Signature"); }); }); From 5dcc7e30533c814941c6f6f71647744ad5422693 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 20 Mar 2026 13:58:30 +0100 Subject: [PATCH 10/31] refactor: don't return classes in UseCases --- .../consumption/openid4vc/VerifyPresentationToken.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts b/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts index b9094cee6..f8c1ebdd9 100644 --- a/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts +++ b/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts @@ -11,7 +11,11 @@ export interface VerifyPresentationTokenRequest { export interface VerifyPresentationTokenResponse { isValid: boolean; - error?: Error; + error?: { + name: string; + message: string; + stack?: string; + }; } class Validator extends SchemaValidator { @@ -34,6 +38,9 @@ export class VerifyPresentationTokenUseCase extends UseCase Date: Fri, 20 Mar 2026 14:23:22 +0100 Subject: [PATCH 11/31] test: re-add test skip --- packages/runtime/test/consumption/openid4vc.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index d06a95392..402f7d36c 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -182,7 +182,9 @@ describe("EUDIPLO", () => { expect(presentationResult.value.status).toBe(200); }); - test("issuance with request", async () => { + // TODO: unskip once fix to CanCreateShareCredentialOffer has been deployed to the connector + // eslint-disable-next-line jest/no-disabled-tests + test.skip("issuance with request", async () => { const oldCredentials = ( await runtimeServices1.consumption.attributes.getAttributes({ query: { From fb57fc7f40035af4d5f41e09c316f4e38f100260 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 20 Mar 2026 14:27:42 +0100 Subject: [PATCH 12/31] chore: build schemas --- .../runtime/src/useCases/common/Schemas.ts | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/packages/runtime/src/useCases/common/Schemas.ts b/packages/runtime/src/useCases/common/Schemas.ts index 663b11f55..72cafac8d 100644 --- a/packages/runtime/src/useCases/common/Schemas.ts +++ b/packages/runtime/src/useCases/common/Schemas.ts @@ -17507,6 +17507,69 @@ export const StoreCredentialsRequest: any = { } } +export const VerifyPresentationTokenRequest: any = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/VerifyPresentationTokenRequest", + "definitions": { + "VerifyPresentationTokenRequest": { + "type": "object", + "properties": { + "tokenContent": { + "$ref": "#/definitions/TokenContentVerifiablePresentationJSON" + }, + "expectedNonce": { + "type": "string" + } + }, + "required": [ + "tokenContent", + "expectedNonce" + ], + "additionalProperties": false + }, + "TokenContentVerifiablePresentationJSON": { + "type": "object", + "properties": { + "@type": { + "type": "string", + "const": "TokenContentVerifiablePresentation" + }, + "@context": { + "type": "string" + }, + "@version": { + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object" + } + ] + }, + "type": { + "type": "string" + }, + "displayInformation": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "required": [ + "@type", + "type", + "value" + ], + "additionalProperties": false + } + } +} + export const CreateSettingRequest: any = { "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/CreateSettingRequest", @@ -20712,69 +20775,6 @@ export const LoadPeerTokenRequest: any = { } } -export const VerifyPresentationTokenRequest: any = { - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/VerifyPresentationTokenRequest", - "definitions": { - "VerifyPresentationTokenRequest": { - "type": "object", - "properties": { - "tokenContent": { - "$ref": "#/definitions/TokenContentVerifiablePresentationJSON" - }, - "expectedNonce": { - "type": "string" - } - }, - "required": [ - "tokenContent", - "expectedNonce" - ], - "additionalProperties": false - }, - "TokenContentVerifiablePresentationJSON": { - "type": "object", - "properties": { - "@type": { - "type": "string", - "const": "TokenContentVerifiablePresentation" - }, - "@context": { - "type": "string" - }, - "@version": { - "type": "string" - }, - "value": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "object" - } - ] - }, - "type": { - "type": "string" - }, - "displayInformation": { - "type": "array", - "items": { - "type": "object" - } - } - }, - "required": [ - "@type", - "type", - "value" - ], - "additionalProperties": false - } - } -} - export const CommunicationLanguage: any = { "$schema": "http://json-schema.org/draft-07/schema#", "$ref": "#/definitions/CommunicationLanguage", From 09d9819e177c5712de278dca54c862ffae24cf1f Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 20 Mar 2026 14:47:34 +0100 Subject: [PATCH 13/31] test: null checks --- packages/runtime/test/anonymous/tokens.test.ts | 6 +++--- .../test/modules/tokens/AnonymousTokenController.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime/test/anonymous/tokens.test.ts b/packages/runtime/test/anonymous/tokens.test.ts index 1a8c99c91..a6b98674b 100644 --- a/packages/runtime/test/anonymous/tokens.test.ts +++ b/packages/runtime/test/anonymous/tokens.test.ts @@ -52,9 +52,9 @@ describe("Anonymous tokens", () => { expect(result).toBeSuccessful(); const token = result.value; - expect(token.passwordProtection.password).toBeDefined(); - expect(token.passwordProtection.passwordIsPin).toBeUndefined(); - expect(token.passwordProtection.passwordLocationIndicator).toBeUndefined(); + expect(token.passwordProtection?.password).toBeDefined(); + expect(token.passwordProtection?.passwordIsPin).toBeUndefined(); + expect(token.passwordProtection?.passwordLocationIndicator).toBeUndefined(); }); test("should get a proper error when trying to load an empty token", async () => { diff --git a/packages/transport/test/modules/tokens/AnonymousTokenController.test.ts b/packages/transport/test/modules/tokens/AnonymousTokenController.test.ts index c3712f04f..20660ed68 100644 --- a/packages/transport/test/modules/tokens/AnonymousTokenController.test.ts +++ b/packages/transport/test/modules/tokens/AnonymousTokenController.test.ts @@ -133,8 +133,8 @@ describe("AnonymousTokenController", function () { test("should create an empty token", async () => { const token = await anonymousTokenController.createEmptyToken(); - expect(token.passwordProtection.password).toBeDefined(); - expect(token.passwordProtection.passwordLocationIndicator).toBeUndefined(); + expect(token.passwordProtection?.password).toBeDefined(); + expect(token.passwordProtection?.passwordLocationIndicator).toBeUndefined(); }); test("should get a proper error when trying to load an empty token", async () => { From ddd726cd8fc7a69dc10c0e79840ef97915cefe34 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Mon, 23 Mar 2026 09:52:58 +0100 Subject: [PATCH 14/31] test: split presentation token test --- .../test/consumption/openid4vc.test.ts | 142 +++++++++--------- 1 file changed, 75 insertions(+), 67 deletions(-) diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 402f7d36c..89e49d885 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -254,78 +254,86 @@ describe("EUDIPLO", () => { expect(sessionStatus).toBe("completed"); // in case of failed presentation: Status remains "active" }); - test("presentation with token", async () => { - const credentialOfferUrl = ( - await eudiploClient.createIssuanceOffer({ - responseType: "uri", - credentialConfigurationIds: [eudiploCredentialConfigurationId], - flow: "pre_authorized_code" - }) - ).uri; - - const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); - const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ - credentialOffer: resolveCredentialOfferResult.value.credentialOffer, - credentialConfigurationIds: [eudiploCredentialConfigurationId] + describe("presentation token", () => { + let presentationTokenId: string; + + test("create presentation token", async () => { + const credentialOfferUrl = ( + await eudiploClient.createIssuanceOffer({ + responseType: "uri", + credentialConfigurationIds: [eudiploCredentialConfigurationId], + flow: "pre_authorized_code" + }) + ).uri; + + const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); + const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ + credentialOffer: resolveCredentialOfferResult.value.credentialOffer, + credentialConfigurationIds: [eudiploCredentialConfigurationId] + }); + const storedCredential = ( + await runtimeServices1.consumption.openId4Vc.storeCredentials({ + credentialResponses: credentialResponsesResult.value.credentialResponses + }) + ).value; + expect((storedCredential.content.value as VerifiableCredentialJSON).displayInformation?.[0].name).toBe("test"); + + const presentationTokenResult = await runtimeServices1.consumption.openId4Vc.createPresentationToken({ + attributeId: storedCredential.id, + expiresAt: CoreDate.utc().add({ minutes: 1 }).toString(), + ephemeral: true + }); + + expect(presentationTokenResult).toBeSuccessful(); + presentationTokenId = presentationTokenResult.value.id; + + const presentationTokenContent = presentationTokenResult.value.content; + expect(presentationTokenContent).toBeDefined(); + expect(presentationTokenContent["@type"]).toBe("TokenContentVerifiablePresentation"); + expect((presentationTokenContent as TokenContentVerifiablePresentation).value).toBeDefined(); + expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation).toBeDefined(); + expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation![0].name).toBe("test"); + + test("verify presentation token", async () => { + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + tokenContent: presentationTokenContent, + expectedNonce: presentationTokenId + }); + + expect(verificationResult).toBeSuccessful(); + expect(verificationResult.value.isValid).toBe(true); + }); }); - const storedCredential = ( - await runtimeServices1.consumption.openId4Vc.storeCredentials({ - credentialResponses: credentialResponsesResult.value.credentialResponses - }) - ).value; - expect((storedCredential.content.value as VerifiableCredentialJSON).displayInformation?.[0].name).toBe("test"); - const presentationTokenResult = await runtimeServices1.consumption.openId4Vc.createPresentationToken({ - attributeId: storedCredential.id, - expiresAt: CoreDate.utc().add({ minutes: 1 }).toString(), - ephemeral: true + test("fail token verification in case of invalid nonce", async () => { + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + tokenContent: { + "@type": "TokenContentVerifiablePresentation", + type: ClaimFormat.SdJwtDc, + value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmZ6Q0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBek1EVXhNVEF4TlRGYUZ3MHlOekF6TURVeE1UQXhOVEZhTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05JQURCRkFpRUEwc1A3MWJXdzhYTTl0czZFT29JSE9Cb1V4V21EbHFVR1dESGp0SDU3S0cwQ0lGTU1sQUdHNElxcloxQ0pGMUJ1eWhWVTNaRWc0ck1acDNHdlgwNkt1UjhyIl0sImFsZyI6IkVTMjU2In0.eyJpYXQiOjE3NzQwMTAwNzgsImV4cCI6MTA0MTQwMTAwNzgsInZjdCI6InRlc3QiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiZlVKTEJZd1Bnb0RTOWRNWDh1TUVLYmpIVXNabGdHYTY2RUdpOEtoSDY1QSIsInkiOiJ4Q3Z1ZENfVDBsOVM5VnRGOGRXVHZrdkl2NUxDcmxWYlM1bDlZYTFQQVh3Iiwia2lkIjoiOTA1N2E5ZGMtMWRiMC00MWI3LWJmMDEtYmM3ODFkOThjMTA3In19LCJfc2RfYWxnIjoic2hhLTI1NiJ9.2u6b8GxPo2uvFHYHrDxfGnR25pgpmo1BZ2VEbb8pLJmPM_oJ9LmcrKpQut2GDi-tCkU6SvEQ7koG4VHPpFgYEA~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzQwMTAwODEuNDgzLCJub25jZSI6IlRPS3VFdWdTWWw2VFlEcmFmUngwIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6InRuVDdWNk9DUGVKeEkyN3JrVnRtcDBtMFAyaUxFMHpVSEN5dk8yNFNhVkUifQ.TloaQCqVBw-u6Zg7hfWnaMPB1od6YN3zqKokQF280SdF1d4H_h6IXvIVj7r89yROEMhl1eFf8zU02CHbkBNfpg" + }, + expectedNonce: "wrong-nonce" + }); + + expect(verificationResult).toBeSuccessful(); + expect(verificationResult.value.isValid).toBe(false); + expect(verificationResult.value.error?.message).toBe("Verify Error: Invalid Nonce"); }); - expect(presentationTokenResult).toBeSuccessful(); - - const presentationTokenContent = presentationTokenResult.value.content; - expect(presentationTokenContent).toBeDefined(); - expect(presentationTokenContent["@type"]).toBe("TokenContentVerifiablePresentation"); - expect((presentationTokenContent as TokenContentVerifiablePresentation).value).toBeDefined(); - expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation).toBeDefined(); - expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation![0].name).toBe("test"); - - const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ - tokenContent: presentationTokenContent, - expectedNonce: presentationTokenResult.value.id - }); - - expect(verificationResult).toBeSuccessful(); - expect(verificationResult.value.isValid).toBe(true); - }); - test("fail token verification in case of invalid nonce", async () => { - const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ - tokenContent: { - "@type": "TokenContentVerifiablePresentation", - type: ClaimFormat.SdJwtDc, - value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmZ6Q0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBek1EVXhNVEF4TlRGYUZ3MHlOekF6TURVeE1UQXhOVEZhTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05JQURCRkFpRUEwc1A3MWJXdzhYTTl0czZFT29JSE9Cb1V4V21EbHFVR1dESGp0SDU3S0cwQ0lGTU1sQUdHNElxcloxQ0pGMUJ1eWhWVTNaRWc0ck1acDNHdlgwNkt1UjhyIl0sImFsZyI6IkVTMjU2In0.eyJpYXQiOjE3NzQwMTAwNzgsImV4cCI6MTA0MTQwMTAwNzgsInZjdCI6InRlc3QiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiZlVKTEJZd1Bnb0RTOWRNWDh1TUVLYmpIVXNabGdHYTY2RUdpOEtoSDY1QSIsInkiOiJ4Q3Z1ZENfVDBsOVM5VnRGOGRXVHZrdkl2NUxDcmxWYlM1bDlZYTFQQVh3Iiwia2lkIjoiOTA1N2E5ZGMtMWRiMC00MWI3LWJmMDEtYmM3ODFkOThjMTA3In19LCJfc2RfYWxnIjoic2hhLTI1NiJ9.2u6b8GxPo2uvFHYHrDxfGnR25pgpmo1BZ2VEbb8pLJmPM_oJ9LmcrKpQut2GDi-tCkU6SvEQ7koG4VHPpFgYEA~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzQwMTAwODEuNDgzLCJub25jZSI6IlRPS3VFdWdTWWw2VFlEcmFmUngwIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6InRuVDdWNk9DUGVKeEkyN3JrVnRtcDBtMFAyaUxFMHpVSEN5dk8yNFNhVkUifQ.TloaQCqVBw-u6Zg7hfWnaMPB1od6YN3zqKokQF280SdF1d4H_h6IXvIVj7r89yROEMhl1eFf8zU02CHbkBNfpg" - }, - expectedNonce: "wrong-nonce" + test("fail token verification in case of invalid signature", async () => { + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + tokenContent: { + "@type": "TokenContentVerifiablePresentation", + type: ClaimFormat.SdJwtDc, + value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmZ6Q0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBek1EVXhNVEF4TlRGYUZ3MHlOekF6TURVeE1UQXhOVEZhTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05JQURCRkFpRUEwc1A3MWJXdzhYTTl0czZFT29JSE9Cb1V4V21EbHFVR1dESGp0SDU3S0cwQ0lGTU1sQUdHNElxcloxQ0pGMUJ1eWhWVTNaRWc0ck1acDNHdlgwNkt1UjhyIl0sImFsZyI6IkVTMjU2In0.eyJpYXQiOjE3NzQwMTAwNzgsImV4cCI6MTA0MTQwMTAwNzgsInZjdCI6InRlc3QiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiZlVKTEJZd1Bnb0RTOWRNWDh1TUVLYmpIVXNabGdHYTY2RUdpOEtoSDY1QSIsInkiOiJ4Q3Z1ZENfVDBsOVM5VnRGOGRXVHZrdkl2NUxDcmxWYlM1bDlZYTFQQVh3Iiwia2lkIjoiOTA1N2E5ZGMtMWRiMC00MWI3LWJmMDEtYmM3ODFkOThjMTA3In19LCJfc2RfYWxnIjoic2hhLTI1NiJ9.2u6b8GxPo2uvFHYHrDxfGnR25pgpmo1BZ2VEbb8pLJmPM_oJ9LmcrKpQut2GDi-tCkU6SvEQ7koG4VHPpFgYE~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzQwMTAwODEuNDgzLCJub25jZSI6IlRPS3VFdWdTWWw2VFlEcmFmUngwIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6InRuVDdWNk9DUGVKeEkyN3JrVnRtcDBtMFAyaUxFMHpVSEN5dk8yNFNhVkUifQ.TloaQCqVBw-u6Zg7hfWnaMPB1od6YN3zqKokQF280SdF1d4H_h6IXvIVj7r89yROEMhl1eFf8zU02CHbkBNfpg" + }, + expectedNonce: "TOKjQHCNf9oXPB0f3SNP" + }); + + expect(verificationResult).toBeSuccessful(); + expect(verificationResult.value.isValid).toBe(false); + expect(verificationResult.value.error?.message).toBe("Verify Error: Invalid JWT Signature"); }); - - expect(verificationResult).toBeSuccessful(); - expect(verificationResult.value.isValid).toBe(false); - expect(verificationResult.value.error?.message).toBe("Verify Error: Invalid Nonce"); - }); - - test("fail token verification in case of invalid signature", async () => { - const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ - tokenContent: { - "@type": "TokenContentVerifiablePresentation", - type: ClaimFormat.SdJwtDc, - value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmZ6Q0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBek1EVXhNVEF4TlRGYUZ3MHlOekF6TURVeE1UQXhOVEZhTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05JQURCRkFpRUEwc1A3MWJXdzhYTTl0czZFT29JSE9Cb1V4V21EbHFVR1dESGp0SDU3S0cwQ0lGTU1sQUdHNElxcloxQ0pGMUJ1eWhWVTNaRWc0ck1acDNHdlgwNkt1UjhyIl0sImFsZyI6IkVTMjU2In0.eyJpYXQiOjE3NzQwMTAwNzgsImV4cCI6MTA0MTQwMTAwNzgsInZjdCI6InRlc3QiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiZlVKTEJZd1Bnb0RTOWRNWDh1TUVLYmpIVXNabGdHYTY2RUdpOEtoSDY1QSIsInkiOiJ4Q3Z1ZENfVDBsOVM5VnRGOGRXVHZrdkl2NUxDcmxWYlM1bDlZYTFQQVh3Iiwia2lkIjoiOTA1N2E5ZGMtMWRiMC00MWI3LWJmMDEtYmM3ODFkOThjMTA3In19LCJfc2RfYWxnIjoic2hhLTI1NiJ9.2u6b8GxPo2uvFHYHrDxfGnR25pgpmo1BZ2VEbb8pLJmPM_oJ9LmcrKpQut2GDi-tCkU6SvEQ7koG4VHPpFgYE~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzQwMTAwODEuNDgzLCJub25jZSI6IlRPS3VFdWdTWWw2VFlEcmFmUngwIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6InRuVDdWNk9DUGVKeEkyN3JrVnRtcDBtMFAyaUxFMHpVSEN5dk8yNFNhVkUifQ.TloaQCqVBw-u6Zg7hfWnaMPB1od6YN3zqKokQF280SdF1d4H_h6IXvIVj7r89yROEMhl1eFf8zU02CHbkBNfpg" - }, - expectedNonce: "TOKjQHCNf9oXPB0f3SNP" - }); - - expect(verificationResult).toBeSuccessful(); - expect(verificationResult.value.isValid).toBe(false); - expect(verificationResult.value.error?.message).toBe("Verify Error: Invalid JWT Signature"); }); }); From bb7a8850434bfed04179a32701955c703ee73ac0 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Mon, 23 Mar 2026 09:53:43 +0100 Subject: [PATCH 15/31] test: move startEudiplo into .dev --- .dev/compose.openid4vc.yml | 2 +- .../config}/test/certs/certificate.json | 0 .../config}/test/clients/test-admin.json | 0 .../config}/test/images/logo.png | Bin .../config}/test/info.json | 0 .../test/issuance/credentials/test.json | 2 +- .../config}/test/issuance/issuance.json | 0 .../config}/test/keys/key.json | 0 .../config}/test/presentation/test.json | 0 .dev/eudiplo/startEudiplo.ts | 25 ++++++++++++++++++ tsconfig.eslint.json | 2 +- 11 files changed, 28 insertions(+), 3 deletions(-) rename .dev/{eudiplo-assets => eudiplo/config}/test/certs/certificate.json (100%) rename .dev/{eudiplo-assets => eudiplo/config}/test/clients/test-admin.json (100%) rename .dev/{eudiplo-assets => eudiplo/config}/test/images/logo.png (100%) rename .dev/{eudiplo-assets => eudiplo/config}/test/info.json (100%) rename .dev/{eudiplo-assets => eudiplo/config}/test/issuance/credentials/test.json (95%) rename .dev/{eudiplo-assets => eudiplo/config}/test/issuance/issuance.json (100%) rename .dev/{eudiplo-assets => eudiplo/config}/test/keys/key.json (100%) rename .dev/{eudiplo-assets => eudiplo/config}/test/presentation/test.json (100%) create mode 100644 .dev/eudiplo/startEudiplo.ts diff --git a/.dev/compose.openid4vc.yml b/.dev/compose.openid4vc.yml index d01184ea3..c4cf0ad0d 100644 --- a/.dev/compose.openid4vc.yml +++ b/.dev/compose.openid4vc.yml @@ -60,7 +60,7 @@ services: ports: - "3000:3000" volumes: - - ./eudiplo-assets:/app/assets/config + - ./eudiplo/config:/app/assets/config networks: default: diff --git a/.dev/eudiplo-assets/test/certs/certificate.json b/.dev/eudiplo/config/test/certs/certificate.json similarity index 100% rename from .dev/eudiplo-assets/test/certs/certificate.json rename to .dev/eudiplo/config/test/certs/certificate.json diff --git a/.dev/eudiplo-assets/test/clients/test-admin.json b/.dev/eudiplo/config/test/clients/test-admin.json similarity index 100% rename from .dev/eudiplo-assets/test/clients/test-admin.json rename to .dev/eudiplo/config/test/clients/test-admin.json diff --git a/.dev/eudiplo-assets/test/images/logo.png b/.dev/eudiplo/config/test/images/logo.png similarity index 100% rename from .dev/eudiplo-assets/test/images/logo.png rename to .dev/eudiplo/config/test/images/logo.png diff --git a/.dev/eudiplo-assets/test/info.json b/.dev/eudiplo/config/test/info.json similarity index 100% rename from .dev/eudiplo-assets/test/info.json rename to .dev/eudiplo/config/test/info.json diff --git a/.dev/eudiplo-assets/test/issuance/credentials/test.json b/.dev/eudiplo/config/test/issuance/credentials/test.json similarity index 95% rename from .dev/eudiplo-assets/test/issuance/credentials/test.json rename to .dev/eudiplo/config/test/issuance/credentials/test.json index a5912abf0..663f93385 100644 --- a/.dev/eudiplo-assets/test/issuance/credentials/test.json +++ b/.dev/eudiplo/config/test/issuance/credentials/test.json @@ -18,5 +18,5 @@ "vct": "test", "keyBinding": true, "statusManagement": false, - "lifeTime": 3600 + "lifeTime": 8640000000 } diff --git a/.dev/eudiplo-assets/test/issuance/issuance.json b/.dev/eudiplo/config/test/issuance/issuance.json similarity index 100% rename from .dev/eudiplo-assets/test/issuance/issuance.json rename to .dev/eudiplo/config/test/issuance/issuance.json diff --git a/.dev/eudiplo-assets/test/keys/key.json b/.dev/eudiplo/config/test/keys/key.json similarity index 100% rename from .dev/eudiplo-assets/test/keys/key.json rename to .dev/eudiplo/config/test/keys/key.json diff --git a/.dev/eudiplo-assets/test/presentation/test.json b/.dev/eudiplo/config/test/presentation/test.json similarity index 100% rename from .dev/eudiplo-assets/test/presentation/test.json rename to .dev/eudiplo/config/test/presentation/test.json diff --git a/.dev/eudiplo/startEudiplo.ts b/.dev/eudiplo/startEudiplo.ts new file mode 100644 index 000000000..115341902 --- /dev/null +++ b/.dev/eudiplo/startEudiplo.ts @@ -0,0 +1,25 @@ +import path from "path"; +import { GenericContainer, StartedTestContainer, Wait } from "testcontainers"; + +export async function startEudiplo(): Promise { + return await new GenericContainer("ghcr.io/openwallet-foundation-labs/eudiplo:3.1.2@sha256:0ea3a73d42a1eb10a6edc45e3289478b08b09064bd75563c503ed12be2ed2dc6") + .withEnvironment({ + PUBLIC_URL: "http://localhost:3000", // eslint-disable-line @typescript-eslint/naming-convention + MASTER_SECRET: "OgwrDcgVQQ2yZwcFt7kPxQm3nUF+X3etF6MdLTstZAY=", // eslint-disable-line @typescript-eslint/naming-convention + AUTH_CLIENT_ID: "root", // eslint-disable-line @typescript-eslint/naming-convention + AUTH_CLIENT_SECRET: "test", // eslint-disable-line @typescript-eslint/naming-convention + CONFIG_IMPORT: "true", // eslint-disable-line @typescript-eslint/naming-convention + CONFIG_FOLDER: "/app/assets/config", // eslint-disable-line @typescript-eslint/naming-convention + PORT: "3000" // eslint-disable-line @typescript-eslint/naming-convention + } as Record) + .withExposedPorts({ container: 3000, host: 3000 }) + .withCopyDirectoriesToContainer([ + { + source: path.resolve(path.join(__dirname, "config")), + target: "/app/assets/config" + } + ]) + .withStartupTimeout(60000) + .withWaitStrategy(Wait.forHealthCheck()) + .start(); +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index d4e68ba12..b5f83a5a3 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -3,5 +3,5 @@ "compilerOptions": { "noEmit": true }, - "include": ["packages/*/src", "packages/*/test"] + "include": ["packages/*/src", "packages/*/test", ".dev/eudiplo"] } From f5b555cdca993735c71e8e2f0d2e1032ec381a76 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Mon, 23 Mar 2026 10:10:07 +0100 Subject: [PATCH 16/31] test: fix the split --- .../test/consumption/openid4vc.test.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 89e49d885..0545534f9 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -8,7 +8,7 @@ import * as client from "openid-client"; import path from "path"; import { DockerComposeEnvironment, StartedDockerComposeEnvironment, Wait } from "testcontainers"; import { Agent as UndiciAgent, fetch as undiciFetch } from "undici"; -import { IncomingRequestStatusChangedEvent } from "../../src"; +import { IncomingRequestStatusChangedEvent, TokenDTO } from "../../src"; import { RuntimeServiceProvider, syncUntilHasMessageWithRequest, syncUntilHasRelationships, TestRuntimeServices } from "../lib"; const fetchInstance: typeof fetch = (async (input: any, init: any) => { @@ -255,7 +255,7 @@ describe("EUDIPLO", () => { }); describe("presentation token", () => { - let presentationTokenId: string; + let presentationToken: TokenDTO; test("create presentation token", async () => { const credentialOfferUrl = ( @@ -285,7 +285,7 @@ describe("EUDIPLO", () => { }); expect(presentationTokenResult).toBeSuccessful(); - presentationTokenId = presentationTokenResult.value.id; + presentationToken = presentationTokenResult.value; const presentationTokenContent = presentationTokenResult.value.content; expect(presentationTokenContent).toBeDefined(); @@ -293,16 +293,16 @@ describe("EUDIPLO", () => { expect((presentationTokenContent as TokenContentVerifiablePresentation).value).toBeDefined(); expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation).toBeDefined(); expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation![0].name).toBe("test"); + }); - test("verify presentation token", async () => { - const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ - tokenContent: presentationTokenContent, - expectedNonce: presentationTokenId - }); - - expect(verificationResult).toBeSuccessful(); - expect(verificationResult.value.isValid).toBe(true); + test("verify presentation token", async () => { + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + tokenContent: presentationToken.content, + expectedNonce: presentationToken.id }); + + expect(verificationResult).toBeSuccessful(); + expect(verificationResult.value.isValid).toBe(true); }); test("fail token verification in case of invalid nonce", async () => { From e17e5183937b59477837536d85f7b8cdc33f37d2 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Mon, 23 Mar 2026 10:13:58 +0100 Subject: [PATCH 17/31] test: remove now obsolete startEudiplo function --- .../test/runtime/AppStringProcessor.test.ts | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index 03977232e..b28bc0e55 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -3,8 +3,7 @@ import { ArbitraryRelationshipTemplateContentJSON, AuthenticationRequestItem, Re import { CoreDate, PasswordLocationIndicatorOptions } from "@nmshd/core-types"; import { DeviceOnboardingInfoDTO, PeerRelationshipTemplateLoadedEvent } from "@nmshd/runtime"; import assert from "assert"; -import path from "path"; -import { GenericContainer, Wait } from "testcontainers"; +import { startEudiplo } from "../../../../.dev/eudiplo/startEudiplo"; import { AppRuntime, LocalAccountSession } from "../../src"; import { MockEventBus, MockUIBridge, TestUtil } from "../lib"; @@ -515,27 +514,4 @@ describe("AppStringProcessor", function () { expect(runtime4MockUiBridge).showFileCalled(file.id); }); }); - - async function startEudiplo() { - return await new GenericContainer("ghcr.io/openwallet-foundation-labs/eudiplo:3.1.2@sha256:0ea3a73d42a1eb10a6edc45e3289478b08b09064bd75563c503ed12be2ed2dc6") - .withEnvironment({ - PUBLIC_URL: "http://localhost:3000", // eslint-disable-line @typescript-eslint/naming-convention - MASTER_SECRET: "OgwrDcgVQQ2yZwcFt7kPxQm3nUF+X3etF6MdLTstZAY=", // eslint-disable-line @typescript-eslint/naming-convention - AUTH_CLIENT_ID: "root", // eslint-disable-line @typescript-eslint/naming-convention - AUTH_CLIENT_SECRET: "test", // eslint-disable-line @typescript-eslint/naming-convention - CONFIG_IMPORT: "true", // eslint-disable-line @typescript-eslint/naming-convention - CONFIG_FOLDER: "/app/assets/config", // eslint-disable-line @typescript-eslint/naming-convention - PORT: "3000" // eslint-disable-line @typescript-eslint/naming-convention - } as Record) - .withExposedPorts({ container: 3000, host: 3000 }) - .withCopyDirectoriesToContainer([ - { - source: path.resolve(path.join(__dirname, "..", "..", "..", "..", ".dev", "eudiplo-assets")), - target: "/app/assets/config" - } - ]) - .withStartupTimeout(60000) - .withWaitStrategy(Wait.forHealthCheck()) - .start(); - } }); From 760543f2cdf6b2b447a4678540f8d5d125e4a85d Mon Sep 17 00:00:00 2001 From: mkuhn Date: Thu, 26 Mar 2026 13:52:05 +0100 Subject: [PATCH 18/31] fix: remove password protection from sendEmptyTokenParameters --- .../src/modules/tokens/local/SendEmptyTokenParameters.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts b/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts index 9fcea53f8..f09cea641 100644 --- a/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts +++ b/packages/transport/src/modules/tokens/local/SendEmptyTokenParameters.ts @@ -4,7 +4,6 @@ import { CoreDate, ICoreDate } from "@nmshd/core-types"; export interface ISendEmptyTokenParameters extends ISerializable { expiresAt: ICoreDate; ephemeral: boolean; - usePasswordProtection?: boolean; } @type("SendEmptyTokenParameters") @@ -17,10 +16,6 @@ export class SendEmptyTokenParameters extends Serializable implements ISendEmpty @serialize() public ephemeral: boolean; - @validate({ nullable: true }) - @serialize() - public usePasswordProtection?: boolean; - public static from(value: ISendEmptyTokenParameters): SendEmptyTokenParameters { return this.fromAny(value); } From 355c4881a52e6670a1a5619c310b890f3a0ceb63 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Thu, 26 Mar 2026 13:53:57 +0100 Subject: [PATCH 19/31] fix: undo credential lifetime increase --- .dev/eudiplo/config/test/issuance/credentials/test.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.dev/eudiplo/config/test/issuance/credentials/test.json b/.dev/eudiplo/config/test/issuance/credentials/test.json index 663f93385..a5912abf0 100644 --- a/.dev/eudiplo/config/test/issuance/credentials/test.json +++ b/.dev/eudiplo/config/test/issuance/credentials/test.json @@ -18,5 +18,5 @@ "vct": "test", "keyBinding": true, "statusManagement": false, - "lifeTime": 8640000000 + "lifeTime": 3600 } From f8441bffc4ff0f50d05056d720063b860dfae826 Mon Sep 17 00:00:00 2001 From: Timo Notheisen Date: Fri, 27 Mar 2026 10:03:06 +0100 Subject: [PATCH 20/31] test: make tests independent of each other --- .../test/consumption/openid4vc.test.ts | 119 +++++++++++------- 1 file changed, 76 insertions(+), 43 deletions(-) diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 0545534f9..23ca94e5a 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -1,14 +1,14 @@ -import { ClaimFormat, SdJwtVcRecord } from "@credo-ts/core"; +import { SdJwtVcRecord } from "@credo-ts/core"; import { EudiploClient } from "@eudiplo/sdk-core"; import { AcceptProposeAttributeRequestItemParametersWithNewAttributeJSON, AcceptShareAuthorizationRequestRequestItemParametersJSON, decodeRecord } from "@nmshd/consumption"; -import { RequestJSON, ShareAuthorizationRequestRequestItemJSON, TokenContentVerifiablePresentation, VerifiableCredentialJSON } from "@nmshd/content"; +import { RequestJSON, ShareAuthorizationRequestRequestItemJSON, TokenContentVerifiablePresentationJSON, VerifiableCredentialJSON } from "@nmshd/content"; import { CoreDate } from "@nmshd/core-types"; import axios, { AxiosInstance } from "axios"; import * as client from "openid-client"; import path from "path"; import { DockerComposeEnvironment, StartedDockerComposeEnvironment, Wait } from "testcontainers"; import { Agent as UndiciAgent, fetch as undiciFetch } from "undici"; -import { IncomingRequestStatusChangedEvent, TokenDTO } from "../../src"; +import { IncomingRequestStatusChangedEvent, LocalAttributeDTO } from "../../src"; import { RuntimeServiceProvider, syncUntilHasMessageWithRequest, syncUntilHasRelationships, TestRuntimeServices } from "../lib"; const fetchInstance: typeof fetch = (async (input: any, init: any) => { @@ -255,47 +255,29 @@ describe("EUDIPLO", () => { }); describe("presentation token", () => { - let presentationToken: TokenDTO; - test("create presentation token", async () => { - const credentialOfferUrl = ( - await eudiploClient.createIssuanceOffer({ - responseType: "uri", - credentialConfigurationIds: [eudiploCredentialConfigurationId], - flow: "pre_authorized_code" - }) - ).uri; - - const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); - const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ - credentialOffer: resolveCredentialOfferResult.value.credentialOffer, - credentialConfigurationIds: [eudiploCredentialConfigurationId] - }); - const storedCredential = ( - await runtimeServices1.consumption.openId4Vc.storeCredentials({ - credentialResponses: credentialResponsesResult.value.credentialResponses - }) - ).value; - expect((storedCredential.content.value as VerifiableCredentialJSON).displayInformation?.[0].name).toBe("test"); - - const presentationTokenResult = await runtimeServices1.consumption.openId4Vc.createPresentationToken({ + const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); + + const createPresentationTokenResult = await runtimeServices1.consumption.openId4Vc.createPresentationToken({ attributeId: storedCredential.id, expiresAt: CoreDate.utc().add({ minutes: 1 }).toString(), ephemeral: true }); - expect(presentationTokenResult).toBeSuccessful(); - presentationToken = presentationTokenResult.value; + expect(createPresentationTokenResult).toBeSuccessful(); - const presentationTokenContent = presentationTokenResult.value.content; + const presentationTokenContent = createPresentationTokenResult.value.content; expect(presentationTokenContent).toBeDefined(); expect(presentationTokenContent["@type"]).toBe("TokenContentVerifiablePresentation"); - expect((presentationTokenContent as TokenContentVerifiablePresentation).value).toBeDefined(); - expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation).toBeDefined(); - expect((presentationTokenContent as TokenContentVerifiablePresentation).displayInformation![0].name).toBe("test"); + expect((presentationTokenContent as TokenContentVerifiablePresentationJSON).value).toBeDefined(); + expect((presentationTokenContent as TokenContentVerifiablePresentationJSON).displayInformation).toBeDefined(); + expect((presentationTokenContent as TokenContentVerifiablePresentationJSON).displayInformation![0].name).toBe("test"); }); test("verify presentation token", async () => { + const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); + const presentationToken = await createPresentationToken(storedCredential); + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ tokenContent: presentationToken.content, expectedNonce: presentationToken.id @@ -306,12 +288,11 @@ describe("EUDIPLO", () => { }); test("fail token verification in case of invalid nonce", async () => { + const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); + const presentationToken = await createPresentationToken(storedCredential); + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ - tokenContent: { - "@type": "TokenContentVerifiablePresentation", - type: ClaimFormat.SdJwtDc, - value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmZ6Q0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBek1EVXhNVEF4TlRGYUZ3MHlOekF6TURVeE1UQXhOVEZhTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05JQURCRkFpRUEwc1A3MWJXdzhYTTl0czZFT29JSE9Cb1V4V21EbHFVR1dESGp0SDU3S0cwQ0lGTU1sQUdHNElxcloxQ0pGMUJ1eWhWVTNaRWc0ck1acDNHdlgwNkt1UjhyIl0sImFsZyI6IkVTMjU2In0.eyJpYXQiOjE3NzQwMTAwNzgsImV4cCI6MTA0MTQwMTAwNzgsInZjdCI6InRlc3QiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiZlVKTEJZd1Bnb0RTOWRNWDh1TUVLYmpIVXNabGdHYTY2RUdpOEtoSDY1QSIsInkiOiJ4Q3Z1ZENfVDBsOVM5VnRGOGRXVHZrdkl2NUxDcmxWYlM1bDlZYTFQQVh3Iiwia2lkIjoiOTA1N2E5ZGMtMWRiMC00MWI3LWJmMDEtYmM3ODFkOThjMTA3In19LCJfc2RfYWxnIjoic2hhLTI1NiJ9.2u6b8GxPo2uvFHYHrDxfGnR25pgpmo1BZ2VEbb8pLJmPM_oJ9LmcrKpQut2GDi-tCkU6SvEQ7koG4VHPpFgYEA~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzQwMTAwODEuNDgzLCJub25jZSI6IlRPS3VFdWdTWWw2VFlEcmFmUngwIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6InRuVDdWNk9DUGVKeEkyN3JrVnRtcDBtMFAyaUxFMHpVSEN5dk8yNFNhVkUifQ.TloaQCqVBw-u6Zg7hfWnaMPB1od6YN3zqKokQF280SdF1d4H_h6IXvIVj7r89yROEMhl1eFf8zU02CHbkBNfpg" - }, + tokenContent: presentationToken.content, expectedNonce: "wrong-nonce" }); @@ -321,13 +302,14 @@ describe("EUDIPLO", () => { }); test("fail token verification in case of invalid signature", async () => { + const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); + const presentationToken = await createPresentationToken(storedCredential); + + const tokenContentWithTamperedSignature = tamperSignatureOfTokenContent(presentationToken.content as TokenContentVerifiablePresentationJSON); + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ - tokenContent: { - "@type": "TokenContentVerifiablePresentation", - type: ClaimFormat.SdJwtDc, - value: "eyJ0eXAiOiJkYytzZC1qd3QiLCJ4NWMiOlsiTUlJQmZ6Q0NBU1dnQXdJQkFnSUJBVEFLQmdncWhrak9QUVFEQWpBY01Rc3dDUVlEVlFRR0V3SkVSVEVOTUFzR0ExVUVBeE1FZEdWemREQWVGdzB5TmpBek1EVXhNVEF4TlRGYUZ3MHlOekF6TURVeE1UQXhOVEZhTUJ3eEN6QUpCZ05WQkFZVEFrUkZNUTB3Q3dZRFZRUURFd1IwWlhOME1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRWRpaTNZRDd1bTNnRmF3MlJuL0FENmczU3J4V0dGQVFOR2p0NzR3VW5DSUNLSzBzNllHUk9GdnliTWhueWNOZkRnL24rZWdTeEllbXo5Q1QyT1hlTmY2TllNRll3RkFZRFZSMFJCQTB3QzRJSmJHOWpZV3hvYjNOME1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWREZ1FXQkJRVlB3YitISUNsOEFmZkVNSFRoTWZUblJNbGpqQUtCZ2dxaGtqT1BRUURBZ05JQURCRkFpRUEwc1A3MWJXdzhYTTl0czZFT29JSE9Cb1V4V21EbHFVR1dESGp0SDU3S0cwQ0lGTU1sQUdHNElxcloxQ0pGMUJ1eWhWVTNaRWc0ck1acDNHdlgwNkt1UjhyIl0sImFsZyI6IkVTMjU2In0.eyJpYXQiOjE3NzQwMTAwNzgsImV4cCI6MTA0MTQwMTAwNzgsInZjdCI6InRlc3QiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiZlVKTEJZd1Bnb0RTOWRNWDh1TUVLYmpIVXNabGdHYTY2RUdpOEtoSDY1QSIsInkiOiJ4Q3Z1ZENfVDBsOVM5VnRGOGRXVHZrdkl2NUxDcmxWYlM1bDlZYTFQQVh3Iiwia2lkIjoiOTA1N2E5ZGMtMWRiMC00MWI3LWJmMDEtYmM3ODFkOThjMTA3In19LCJfc2RfYWxnIjoic2hhLTI1NiJ9.2u6b8GxPo2uvFHYHrDxfGnR25pgpmo1BZ2VEbb8pLJmPM_oJ9LmcrKpQut2GDi-tCkU6SvEQ7koG4VHPpFgYE~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3NzQwMTAwODEuNDgzLCJub25jZSI6IlRPS3VFdWdTWWw2VFlEcmFmUngwIiwiYXVkIjoiZGVmYXVsdFByZXNlbnRhdGlvbkF1ZGllbmNlIiwic2RfaGFzaCI6InRuVDdWNk9DUGVKeEkyN3JrVnRtcDBtMFAyaUxFMHpVSEN5dk8yNFNhVkUifQ.TloaQCqVBw-u6Zg7hfWnaMPB1od6YN3zqKokQF280SdF1d4H_h6IXvIVj7r89yROEMhl1eFf8zU02CHbkBNfpg" - }, - expectedNonce: "TOKjQHCNf9oXPB0f3SNP" + tokenContent: tokenContentWithTamperedSignature, + expectedNonce: presentationToken.id }); expect(verificationResult).toBeSuccessful(); @@ -337,6 +319,57 @@ describe("EUDIPLO", () => { }); }); +async function createPresentationToken(storedCredential: LocalAttributeDTO) { + const result = await runtimeServices1.consumption.openId4Vc.createPresentationToken({ + attributeId: storedCredential.id, + expiresAt: CoreDate.utc().add({ minutes: 1 }).toString(), + ephemeral: true + }); + + return result.value; +} + +async function createAndStoreCredential(eudiploClient: EudiploClient, eudiploCredentialConfigurationId: string) { + const credentialOfferUrl = ( + await eudiploClient.createIssuanceOffer({ + responseType: "uri", + credentialConfigurationIds: [eudiploCredentialConfigurationId], + flow: "pre_authorized_code" + }) + ).uri; + + const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); + const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ + credentialOffer: resolveCredentialOfferResult.value.credentialOffer, + credentialConfigurationIds: [eudiploCredentialConfigurationId] + }); + const storedCredential = ( + await runtimeServices1.consumption.openId4Vc.storeCredentials({ + credentialResponses: credentialResponsesResult.value.credentialResponses + }) + ).value; + return storedCredential; +} + +function tamperSignatureOfTokenContent(tokenContent: TokenContentVerifiablePresentationJSON): TokenContentVerifiablePresentationJSON { + const splittedValue = tokenContent.value.split("."); + + const header = splittedValue[0]; + const payload = splittedValue[1]; + const disclosure = splittedValue[3]; + const keyBindingJWT = splittedValue[4]; + + // the following is a signature of some old SD-JWT that we use here just to have a signature that is valid in structure but does not match the + // header and payload of the token content, thus leading to a failed verification due to invalid signature + const tamperedSignature = "V6RFMHpLyj2NOi4BphSygcbXxWvBeArY9zdkUGj-ERJO9S3CgGxst8lGyV0DJMT7N_-85kIDcukHDw2ia9KITQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9"; + + const tamperedTokenContent = { + ...tokenContent, + value: `${header}.${payload}.${tamperedSignature}.${disclosure}.${keyBindingJWT}` + }; + return tamperedTokenContent; +} + async function startOid4VcComposeStack() { let baseUrl = process.env.NMSHD_TEST_BASEURL!; let addressGenerationHostnameOverride: string | undefined; From f47ac356ce8c9afa29bd53252edcf9d5fe378407 Mon Sep 17 00:00:00 2001 From: Timo Notheisen Date: Fri, 27 Mar 2026 10:03:22 +0100 Subject: [PATCH 21/31] test: improve startOid4VcComposeStack helper method --- .../test/consumption/openid4vc.test.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 23ca94e5a..4453ab32a 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -372,27 +372,25 @@ function tamperSignatureOfTokenContent(tokenContent: TokenContentVerifiablePrese async function startOid4VcComposeStack() { let baseUrl = process.env.NMSHD_TEST_BASEURL!; - let addressGenerationHostnameOverride: string | undefined; + + const composeEnvironment = { + // eslint-disable-next-line @typescript-eslint/naming-convention + TEST_ENVIRONMENT: "container", + // eslint-disable-next-line @typescript-eslint/naming-convention + NMSHD_TEST_BASEURL: baseUrl + } as Record; if (baseUrl.includes("localhost")) { - addressGenerationHostnameOverride = "localhost"; + composeEnvironment.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE = "localhost"; baseUrl = baseUrl.replace("localhost", "host.docker.internal"); - } else { - addressGenerationHostnameOverride = process.env.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE; + } else if (process.env.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE) { + composeEnvironment.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE = process.env.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE; } const composeFolder = path.resolve(path.join(__dirname, "..", "..", "..", "..", ".dev")); const composeStack = await new DockerComposeEnvironment(composeFolder, "compose.openid4vc.yml") .withProjectName("runtime-oid4vc-tests") - .withEnvironment({ - // eslint-disable-next-line @typescript-eslint/naming-convention - TEST_ENVIRONMENT: "container", - // eslint-disable-next-line @typescript-eslint/naming-convention - NMSHD_TEST_BASEURL: baseUrl, - - // eslint-disable-next-line @typescript-eslint/naming-convention - NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE: addressGenerationHostnameOverride - } as Record) + .withEnvironment(composeEnvironment) .withStartupTimeout(60000) .withWaitStrategy("oid4vc-service-1", Wait.forHealthCheck()) .up(); From c535f4228aa4c7d19c2b54faad4d1a1797a40dfe Mon Sep 17 00:00:00 2001 From: Timo Notheisen Date: Fri, 27 Mar 2026 10:08:59 +0100 Subject: [PATCH 22/31] chore: npm audit fix --- package-lock.json | 163 +++++++++++++++++++++++++++++++--------------- 1 file changed, 109 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9fade5242..459015059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3280,7 +3280,9 @@ } }, "node_modules/@npmcli/package-json/node_modules/brace-expansion": { - "version": "5.0.3", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4117,7 +4119,9 @@ } }, "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "5.0.3", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4435,15 +4439,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/type-utils": "8.56.0", - "@typescript-eslint/utils": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -4456,7 +4462,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.0", + "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -4470,14 +4476,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "engines": { @@ -4493,12 +4501,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.0", - "@typescript-eslint/types": "^8.56.0", + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "engines": { @@ -4513,12 +4523,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4529,7 +4541,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", "engines": { @@ -4544,13 +4558,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -4567,7 +4583,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", "engines": { @@ -4579,16 +4597,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.0", - "@typescript-eslint/tsconfig-utils": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/visitor-keys": "8.56.0", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -4604,23 +4624,40 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4638,14 +4675,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.0", - "@typescript-eslint/types": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4660,11 +4699,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -5869,7 +5910,9 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "5.0.3", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7902,7 +7945,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -7953,7 +7998,9 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8183,7 +8230,9 @@ } }, "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "5.0.3", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11388,7 +11437,9 @@ } }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -12835,7 +12886,9 @@ } }, "node_modules/ts-json-schema-generator/node_modules/brace-expansion": { - "version": "5.0.4", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13093,14 +13146,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.0", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.0", - "@typescript-eslint/parser": "8.56.0", - "@typescript-eslint/typescript-estree": "8.56.0", - "@typescript-eslint/utils": "8.56.0" + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" From 3a712d0b841a087cb68b6690ec5903cc2710e3e0 Mon Sep 17 00:00:00 2001 From: Timo Notheisen Date: Fri, 27 Mar 2026 10:09:06 +0100 Subject: [PATCH 23/31] chore: ignore vulnerability --- .ci/runChecks.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/runChecks.sh b/.ci/runChecks.sh index f94dfef62..3422cf91e 100755 --- a/.ci/runChecks.sh +++ b/.ci/runChecks.sh @@ -6,4 +6,4 @@ npm run lint:eslint npm run lint:prettier npm run --workspaces cdep npx --workspaces license-check -npx better-npm-audit audit --exclude 1112030 +npx better-npm-audit audit --exclude 1112030,1115432 From f75d8f13dd70e0af5cd2ccd88329bd37165491e6 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 27 Mar 2026 10:26:37 +0100 Subject: [PATCH 24/31] test: rename eudiplo tenant --- .../{test => test-tenant}/certs/certificate.json | 0 .../{test => test-tenant}/clients/test-admin.json | 0 .../config/{test => test-tenant}/images/logo.png | Bin .dev/eudiplo/config/{test => test-tenant}/info.json | 2 +- .../issuance/credentials/test.json | 0 .../{test => test-tenant}/issuance/issuance.json | 0 .../config/{test => test-tenant}/keys/key.json | 0 .../{test => test-tenant}/presentation/test.json | 0 8 files changed, 1 insertion(+), 1 deletion(-) rename .dev/eudiplo/config/{test => test-tenant}/certs/certificate.json (100%) rename .dev/eudiplo/config/{test => test-tenant}/clients/test-admin.json (100%) rename .dev/eudiplo/config/{test => test-tenant}/images/logo.png (100%) rename .dev/eudiplo/config/{test => test-tenant}/info.json (57%) rename .dev/eudiplo/config/{test => test-tenant}/issuance/credentials/test.json (100%) rename .dev/eudiplo/config/{test => test-tenant}/issuance/issuance.json (100%) rename .dev/eudiplo/config/{test => test-tenant}/keys/key.json (100%) rename .dev/eudiplo/config/{test => test-tenant}/presentation/test.json (100%) diff --git a/.dev/eudiplo/config/test/certs/certificate.json b/.dev/eudiplo/config/test-tenant/certs/certificate.json similarity index 100% rename from .dev/eudiplo/config/test/certs/certificate.json rename to .dev/eudiplo/config/test-tenant/certs/certificate.json diff --git a/.dev/eudiplo/config/test/clients/test-admin.json b/.dev/eudiplo/config/test-tenant/clients/test-admin.json similarity index 100% rename from .dev/eudiplo/config/test/clients/test-admin.json rename to .dev/eudiplo/config/test-tenant/clients/test-admin.json diff --git a/.dev/eudiplo/config/test/images/logo.png b/.dev/eudiplo/config/test-tenant/images/logo.png similarity index 100% rename from .dev/eudiplo/config/test/images/logo.png rename to .dev/eudiplo/config/test-tenant/images/logo.png diff --git a/.dev/eudiplo/config/test/info.json b/.dev/eudiplo/config/test-tenant/info.json similarity index 57% rename from .dev/eudiplo/config/test/info.json rename to .dev/eudiplo/config/test-tenant/info.json index 22e908c4a..3b1aa63ae 100644 --- a/.dev/eudiplo/config/test/info.json +++ b/.dev/eudiplo/config/test-tenant/info.json @@ -1,4 +1,4 @@ { - "name": "test", + "name": "test-tenant", "description": "test tenant" } diff --git a/.dev/eudiplo/config/test/issuance/credentials/test.json b/.dev/eudiplo/config/test-tenant/issuance/credentials/test.json similarity index 100% rename from .dev/eudiplo/config/test/issuance/credentials/test.json rename to .dev/eudiplo/config/test-tenant/issuance/credentials/test.json diff --git a/.dev/eudiplo/config/test/issuance/issuance.json b/.dev/eudiplo/config/test-tenant/issuance/issuance.json similarity index 100% rename from .dev/eudiplo/config/test/issuance/issuance.json rename to .dev/eudiplo/config/test-tenant/issuance/issuance.json diff --git a/.dev/eudiplo/config/test/keys/key.json b/.dev/eudiplo/config/test-tenant/keys/key.json similarity index 100% rename from .dev/eudiplo/config/test/keys/key.json rename to .dev/eudiplo/config/test-tenant/keys/key.json diff --git a/.dev/eudiplo/config/test/presentation/test.json b/.dev/eudiplo/config/test-tenant/presentation/test.json similarity index 100% rename from .dev/eudiplo/config/test/presentation/test.json rename to .dev/eudiplo/config/test-tenant/presentation/test.json From 1b66718e1242c8db868f021ad4bed3df67ba818d Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 27 Mar 2026 10:50:08 +0100 Subject: [PATCH 25/31] test: namings in app string processor test --- .../test/runtime/AppStringProcessor.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts index b28bc0e55..aecfc06b3 100644 --- a/packages/app-runtime/test/runtime/AppStringProcessor.test.ts +++ b/packages/app-runtime/test/runtime/AppStringProcessor.test.ts @@ -383,7 +383,7 @@ describe("AppStringProcessor", function () { const eudiploClientId = "test-admin"; const eudiploClientSecret = "57c9cd444bf402b2cc1f5a0d2dafd3955bd9042c0372db17a4ede2d5fbda88e5"; const eudiploCredentialConfigurationId = "test"; - const eudiploBaseUrl = `http://localhost:3000`; + const eudiploBaseUrl = "http://localhost:3000"; const eudiploContainer = await startEudiplo(); @@ -402,22 +402,22 @@ describe("AppStringProcessor", function () { ).uri; const resolveCredentialOfferResult = await runtime1Session.consumptionServices.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); - const credentialResponsesResult = await runtime1Session.consumptionServices.openId4Vc.requestCredentials({ + const requestCredentialsResult = await runtime1Session.consumptionServices.openId4Vc.requestCredentials({ credentialOffer: resolveCredentialOfferResult.value.credentialOffer, credentialConfigurationIds: [eudiploCredentialConfigurationId] }); const storedCredential = ( await runtime1Session.consumptionServices.openId4Vc.storeCredentials({ - credentialResponses: credentialResponsesResult.value.credentialResponses + credentialResponses: requestCredentialsResult.value.credentialResponses }) ).value; - const tokenResult = await runtime1Session.consumptionServices.openId4Vc.createPresentationToken({ + const createPresentationTokenResult = await runtime1Session.consumptionServices.openId4Vc.createPresentationToken({ attributeId: storedCredential.id, expiresAt: CoreDate.utc().add({ days: 1 }).toISOString(), ephemeral: true }); - const token = tokenResult.value; + const token = createPresentationTokenResult.value; const result = await runtime4.stringProcessor.processURL(token.reference.url, runtime4Session.account); expect(result).toBeSuccessful(); From f1b73a5e7a96f02edde8b99443e09eb223d3167a Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 27 Mar 2026 10:57:49 +0100 Subject: [PATCH 26/31] feat: simplify use case interface --- .../app-runtime/src/AppStringProcessor.ts | 5 +- .../runtime/src/useCases/common/Schemas.ts | 121 +++++++++++++++++- .../openid4vc/VerifyPresentationToken.ts | 9 +- 3 files changed, 118 insertions(+), 17 deletions(-) diff --git a/packages/app-runtime/src/AppStringProcessor.ts b/packages/app-runtime/src/AppStringProcessor.ts index 4246f5479..589f842e1 100644 --- a/packages/app-runtime/src/AppStringProcessor.ts +++ b/packages/app-runtime/src/AppStringProcessor.ts @@ -294,10 +294,7 @@ export class AppStringProcessor { const tokenContent = this.parseTokenContent(token.content); if (tokenContent instanceof TokenContentVerifiablePresentation) { - const verificationResult = await services.consumptionServices.openId4Vc.verifyPresentationToken({ - tokenContent: tokenContent.toJSON(), - expectedNonce: token.id - }); + const verificationResult = await services.consumptionServices.openId4Vc.verifyPresentationToken({ token }); await uiBridge.showVerifiablePresentation(account, result.value.value, verificationResult.value.isValid); break; } diff --git a/packages/runtime/src/useCases/common/Schemas.ts b/packages/runtime/src/useCases/common/Schemas.ts index 72cafac8d..5605b2943 100644 --- a/packages/runtime/src/useCases/common/Schemas.ts +++ b/packages/runtime/src/useCases/common/Schemas.ts @@ -17514,16 +17514,72 @@ export const VerifyPresentationTokenRequest: any = { "VerifyPresentationTokenRequest": { "type": "object", "properties": { - "tokenContent": { - "$ref": "#/definitions/TokenContentVerifiablePresentationJSON" - }, - "expectedNonce": { - "type": "string" + "token": { + "type": "object", + "additionalProperties": false, + "properties": { + "content": { + "$ref": "#/definitions/TokenContentVerifiablePresentationJSON" + }, + "id": { + "type": "string" + }, + "isOwn": { + "type": "boolean" + }, + "createdBy": { + "type": "string" + }, + "createdByDevice": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "expiresAt": { + "type": "string" + }, + "forIdentity": { + "type": "string" + }, + "passwordProtection": { + "$ref": "#/definitions/PasswordProtectionDTO" + }, + "reference": { + "type": "object", + "properties": { + "truncated": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": [ + "truncated", + "url" + ], + "additionalProperties": false + }, + "isEphemeral": { + "type": "boolean" + } + }, + "required": [ + "content", + "createdAt", + "createdBy", + "createdByDevice", + "expiresAt", + "id", + "isEphemeral", + "isOwn", + "reference" + ] } }, "required": [ - "tokenContent", - "expectedNonce" + "token" ], "additionalProperties": false }, @@ -17566,6 +17622,57 @@ export const VerifyPresentationTokenRequest: any = { "value" ], "additionalProperties": false + }, + "PasswordProtectionDTO": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "passwordIsPin": { + "type": "boolean", + "const": true + }, + "passwordLocationIndicator": { + "anyOf": [ + { + "type": "string", + "const": "RecoveryKit" + }, + { + "type": "string", + "const": "Self" + }, + { + "type": "string", + "const": "Letter" + }, + { + "type": "string", + "const": "RegistrationLetter" + }, + { + "type": "string", + "const": "Email" + }, + { + "type": "string", + "const": "SMS" + }, + { + "type": "string", + "const": "Website" + }, + { + "type": "number" + } + ] + } + }, + "required": [ + "password" + ], + "additionalProperties": false } } } diff --git a/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts b/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts index f8c1ebdd9..e6cef2dc1 100644 --- a/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts +++ b/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts @@ -1,12 +1,12 @@ import { Result } from "@js-soft/ts-utils"; import { OpenId4VcController } from "@nmshd/consumption"; import { TokenContentVerifiablePresentation, TokenContentVerifiablePresentationJSON } from "@nmshd/content"; +import { TokenDTO } from "@nmshd/runtime-types"; import { Inject } from "@nmshd/typescript-ioc"; import { SchemaRepository, SchemaValidator, UseCase } from "../../common"; export interface VerifyPresentationTokenRequest { - tokenContent: TokenContentVerifiablePresentationJSON; - expectedNonce: string; + token: Omit & { content: TokenContentVerifiablePresentationJSON }; } export interface VerifyPresentationTokenResponse { @@ -33,10 +33,7 @@ export class VerifyPresentationTokenUseCase extends UseCase> { - const verificationResult = await this.openId4VcController.verifyPresentationTokenContent( - TokenContentVerifiablePresentation.from(request.tokenContent), - request.expectedNonce - ); + const verificationResult = await this.openId4VcController.verifyPresentationTokenContent(TokenContentVerifiablePresentation.from(request.token.content), request.token.id); return Result.ok({ isValid: verificationResult.isValid, From b9c59f6605cfa47f73c8a5c1c92fe5fe552e4065 Mon Sep 17 00:00:00 2001 From: Timo Notheisen Date: Fri, 27 Mar 2026 10:58:44 +0100 Subject: [PATCH 27/31] test: set correct NMSHD_TEST_BASEURL in oid4vc compose stack --- packages/runtime/test/consumption/openid4vc.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 4453ab32a..208d324df 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -371,7 +371,7 @@ function tamperSignatureOfTokenContent(tokenContent: TokenContentVerifiablePrese } async function startOid4VcComposeStack() { - let baseUrl = process.env.NMSHD_TEST_BASEURL!; + const baseUrl = process.env.NMSHD_TEST_BASEURL!; const composeEnvironment = { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -382,7 +382,7 @@ async function startOid4VcComposeStack() { if (baseUrl.includes("localhost")) { composeEnvironment.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE = "localhost"; - baseUrl = baseUrl.replace("localhost", "host.docker.internal"); + composeEnvironment.NMSHD_TEST_BASEURL = baseUrl.replace("localhost", "host.docker.internal"); } else if (process.env.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE) { composeEnvironment.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE = process.env.NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE; } From 9b618b9bfce58149f51bb30258b0ab55de2e3331 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 27 Mar 2026 11:28:36 +0100 Subject: [PATCH 28/31] fix: remove stack from error message --- .../useCases/consumption/openid4vc/VerifyPresentationToken.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts b/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts index e6cef2dc1..82119b55f 100644 --- a/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts +++ b/packages/runtime/src/useCases/consumption/openid4vc/VerifyPresentationToken.ts @@ -14,7 +14,6 @@ export interface VerifyPresentationTokenResponse { error?: { name: string; message: string; - stack?: string; }; } @@ -37,7 +36,7 @@ export class VerifyPresentationTokenUseCase extends UseCase Date: Fri, 27 Mar 2026 11:56:44 +0100 Subject: [PATCH 29/31] test: adapt runtime tests --- .../test/consumption/openid4vc.test.ts | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 208d324df..09c1599d9 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -279,28 +279,13 @@ describe("EUDIPLO", () => { const presentationToken = await createPresentationToken(storedCredential); const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ - tokenContent: presentationToken.content, - expectedNonce: presentationToken.id + token: presentationToken }); expect(verificationResult).toBeSuccessful(); expect(verificationResult.value.isValid).toBe(true); }); - test("fail token verification in case of invalid nonce", async () => { - const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); - const presentationToken = await createPresentationToken(storedCredential); - - const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ - tokenContent: presentationToken.content, - expectedNonce: "wrong-nonce" - }); - - expect(verificationResult).toBeSuccessful(); - expect(verificationResult.value.isValid).toBe(false); - expect(verificationResult.value.error?.message).toBe("Verify Error: Invalid Nonce"); - }); - test("fail token verification in case of invalid signature", async () => { const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); const presentationToken = await createPresentationToken(storedCredential); @@ -308,8 +293,7 @@ describe("EUDIPLO", () => { const tokenContentWithTamperedSignature = tamperSignatureOfTokenContent(presentationToken.content as TokenContentVerifiablePresentationJSON); const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ - tokenContent: tokenContentWithTamperedSignature, - expectedNonce: presentationToken.id + token: { ...presentationToken, content: tokenContentWithTamperedSignature } }); expect(verificationResult).toBeSuccessful(); From d02508d786973a31f224ad4c0d0f610c41d97fa1 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 27 Mar 2026 13:15:07 +0100 Subject: [PATCH 30/31] test: re-add invalid nonce test --- packages/runtime/test/consumption/openid4vc.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 09c1599d9..1111ae38c 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -286,6 +286,19 @@ describe("EUDIPLO", () => { expect(verificationResult.value.isValid).toBe(true); }); + test("fail token verification in case of invalid nonce", async () => { + const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); + const presentationToken = await createPresentationToken(storedCredential); + + const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ + token: { ...presentationToken, id: "TOKXXXXXXXXXXXXXXXXX" } + }); + + expect(verificationResult).toBeSuccessful(); + expect(verificationResult.value.isValid).toBe(false); + expect(verificationResult.value.error?.message).toBe("Verify Error: Invalid Nonce"); + }); + test("fail token verification in case of invalid signature", async () => { const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); const presentationToken = await createPresentationToken(storedCredential); From 721f7c72088450b9303e88ba6a822ea82f65b7f1 Mon Sep 17 00:00:00 2001 From: mkuhn Date: Fri, 27 Mar 2026 13:27:40 +0100 Subject: [PATCH 31/31] test: use new helper functions more --- .../test/consumption/openid4vc.test.ts | 283 +++++++++--------- 1 file changed, 145 insertions(+), 138 deletions(-) diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index 1111ae38c..ab011828d 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -16,6 +16,14 @@ const fetchInstance: typeof fetch = (async (input: any, init: any) => { return response; }) as unknown as typeof fetch; +const eudiploClientId = "test-admin"; +const eudiploClientSecret = "57c9cd444bf402b2cc1f5a0d2dafd3955bd9042c0372db17a4ede2d5fbda88e5"; + +const eudiploPresentationConfigurationId = "test"; +const eudiploCredentialConfigurationId = "test"; + +let eudiploClient: EudiploClient; + const runtimeServiceProvider = new RuntimeServiceProvider(fetchInstance); let runtimeServices1: TestRuntimeServices; @@ -41,6 +49,14 @@ beforeAll(async () => { } }); await createActiveRelationshipToService(runtimeServices1, serviceAxiosInstance); + + const eudiploBaseUrl = "http://localhost:3000"; + + eudiploClient = new EudiploClient({ + baseUrl: eudiploBaseUrl, + clientId: eudiploClientId, + clientSecret: eudiploClientSecret + }); }, 120000); afterAll(async () => { @@ -49,117 +65,149 @@ afterAll(async () => { if (dockerComposeStack) await dockerComposeStack.down(); }); -describe("EUDIPLO", () => { - const clientId = "test-admin"; - const clientSecret = "57c9cd444bf402b2cc1f5a0d2dafd3955bd9042c0372db17a4ede2d5fbda88e5"; +test("issuance", async () => { + const credentialOfferUrl = ( + await eudiploClient.createIssuanceOffer({ + responseType: "uri", + credentialConfigurationIds: [eudiploCredentialConfigurationId], + flow: "pre_authorized_code" + }) + ).uri; - const eudiploPresentationConfigurationId = "test"; - const eudiploCredentialConfigurationId = "test"; + const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); + expect(resolveCredentialOfferResult).toBeSuccessful(); - let eudiploClient: EudiploClient; + const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ + credentialOffer: resolveCredentialOfferResult.value.credentialOffer, + credentialConfigurationIds: [eudiploCredentialConfigurationId] + }); + const storeCredentialsResponse = await runtimeServices1.consumption.openId4Vc.storeCredentials({ + credentialResponses: credentialResponsesResult.value.credentialResponses + }); + expect(storeCredentialsResponse).toBeSuccessful(); + expect((storeCredentialsResponse.value.content.value as VerifiableCredentialJSON).displayInformation?.[0].logo).toBeDefined(); + expect((storeCredentialsResponse.value.content.value as VerifiableCredentialJSON).displayInformation?.[0].name).toBe("test"); +}); - beforeAll(() => { - const baseUrl = `http://localhost:3000`; +test("issuance with pin authentication", async () => { + const pin = "1234"; - eudiploClient = new EudiploClient({ - baseUrl, - clientId, - clientSecret - }); + const credentialOfferUrl = ( + await eudiploClient.createIssuanceOffer({ + responseType: "uri", + credentialConfigurationIds: [eudiploCredentialConfigurationId], + flow: "pre_authorized_code", + txCode: pin + }) + ).uri; + + const result = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ + credentialOfferUrl }); - test("issuance", async () => { - const credentialOfferUrl = ( - await eudiploClient.createIssuanceOffer({ - responseType: "uri", - credentialConfigurationIds: [eudiploCredentialConfigurationId], - flow: "pre_authorized_code" - }) - ).uri; + expect(result).toBeSuccessful(); - const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); - expect(resolveCredentialOfferResult).toBeSuccessful(); + const credentialOffer = result.value.credentialOffer; + const requestedCredentials = credentialOffer.credentialOfferPayload.credential_configuration_ids; - const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ - credentialOffer: resolveCredentialOfferResult.value.credentialOffer, - credentialConfigurationIds: [eudiploCredentialConfigurationId] - }); - const storeCredentialsResponse = await runtimeServices1.consumption.openId4Vc.storeCredentials({ - credentialResponses: credentialResponsesResult.value.credentialResponses - }); - expect(storeCredentialsResponse).toBeSuccessful(); - expect((storeCredentialsResponse.value.content.value as VerifiableCredentialJSON).displayInformation?.[0].logo).toBeDefined(); - expect((storeCredentialsResponse.value.content.value as VerifiableCredentialJSON).displayInformation?.[0].name).toBe("test"); + const wrongPinRequestResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ + credentialOffer, + credentialConfigurationIds: requestedCredentials, + pinCode: `1${pin}` }); + expect(wrongPinRequestResult.isError).toBe(true); - test("issuance with pin authentication", async () => { - const pin = "1234"; + const requestResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ + credentialOffer, + credentialConfigurationIds: requestedCredentials, + pinCode: pin + }); + expect(requestResult).toBeSuccessful(); +}); - const credentialOfferUrl = ( - await eudiploClient.createIssuanceOffer({ - responseType: "uri", - credentialConfigurationIds: [eudiploCredentialConfigurationId], - flow: "pre_authorized_code", - txCode: pin - }) - ).uri; +// external authentication buggy in the latest release (1.16.0) +// eslint-disable-next-line jest/no-disabled-tests +test.skip("issuance with external authentication", async () => { + const credentialOfferUrl = ( + await eudiploClient.createIssuanceOffer({ + responseType: "uri", + credentialConfigurationIds: [eudiploCredentialConfigurationId], + flow: "authorization_code" + }) + ).uri; - const result = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ - credentialOfferUrl - }); + const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); + expect(resolveCredentialOfferResult).toBeSuccessful(); + + const server = URL.parse("https://kc-openid4vc.is.enmeshed.eu/realms/enmeshed-openid4vci")!; + const clientId = "wallet"; + const config: client.Configuration = await client.discovery(server, clientId); + const grantReq = await client.genericGrantRequest(config, "password", { + username: "test", + password: "test", + scope: "wallet-demo" + }); - expect(result).toBeSuccessful(); + const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ + credentialOffer: resolveCredentialOfferResult.value.credentialOffer, + credentialConfigurationIds: [eudiploCredentialConfigurationId], + accessToken: grantReq.access_token + }); + expect(credentialResponsesResult).toBeSuccessful(); +}); - const credentialOffer = result.value.credentialOffer; - const requestedCredentials = credentialOffer.credentialOfferPayload.credential_configuration_ids; +// TODO: unskip once fix to CanCreateShareCredentialOffer has been deployed to the connector +// eslint-disable-next-line jest/no-disabled-tests +test.skip("issuance with request", async () => { + const oldCredentials = ( + await runtimeServices1.consumption.attributes.getAttributes({ + query: { + "content.value.@type": "VerifiableCredential" + } + }) + ).value; - const wrongPinRequestResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ - credentialOffer, - credentialConfigurationIds: requestedCredentials, - pinCode: `1${pin}` - }); - expect(wrongPinRequestResult.isError).toBe(true); + const sentMessage = ( + await serviceAxiosInstance.post("/enmeshed-demo/credential", { + recipient: runtimeServices1.address, + credentialConfigurationId: eudiploCredentialConfigurationId + }) + ).data.result; - const requestResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ - credentialOffer, - credentialConfigurationIds: requestedCredentials, - pinCode: pin - }); - expect(requestResult).toBeSuccessful(); + const requestId = (sentMessage.content as RequestJSON).id!; + await syncUntilHasMessageWithRequest(runtimeServices1.transport, requestId); + await runtimeServices1.consumption.incomingRequests.accept({ + requestId, + items: [{ accept: true }] }); - // external authentication buggy in the latest release (1.16.0) - // eslint-disable-next-line jest/no-disabled-tests - test.skip("issuance with external authentication", async () => { - const credentialOfferUrl = ( - await eudiploClient.createIssuanceOffer({ - responseType: "uri", - credentialConfigurationIds: [eudiploCredentialConfigurationId], - flow: "authorization_code" - }) - ).uri; + const currentCredentials = ( + await runtimeServices1.consumption.attributes.getAttributes({ + query: { + "content.value.@type": "VerifiableCredential" + } + }) + ).value; + expect(currentCredentials).toHaveLength(oldCredentials.length + 1); - const resolveCredentialOfferResult = await runtimeServices1.consumption.openId4Vc.resolveCredentialOffer({ credentialOfferUrl }); - expect(resolveCredentialOfferResult).toBeSuccessful(); + const oldCredentialIds = oldCredentials.map((c) => c.id); + const createdCredential = currentCredentials.find((c) => !oldCredentialIds.includes(c.id)); + expect(createdCredential).toBeDefined(); - const server = URL.parse("https://kc-openid4vc.is.enmeshed.eu/realms/enmeshed-openid4vci")!; - const clientId = "wallet"; - const config: client.Configuration = await client.discovery(server, clientId); - const grantReq = await client.genericGrantRequest(config, "password", { - username: "test", - password: "test", - scope: "wallet-demo" - }); + const credentialContent = createdCredential!.content.value as VerifiableCredentialJSON; + const decodedCredential = decodeRecord(credentialContent.type, credentialContent.value) as SdJwtVcRecord; + expect(decodedCredential.firstCredential.prettyClaims.givenName).toBe("aGivenName"); + expect(credentialContent.value.split("~")).toHaveLength(3); // given name is selectively disclosable, hence length 3 +}); - const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ - credentialOffer: resolveCredentialOfferResult.value.credentialOffer, - credentialConfigurationIds: [eudiploCredentialConfigurationId], - accessToken: grantReq.access_token - }); - expect(credentialResponsesResult).toBeSuccessful(); +describe("presentation", () => { + let storedCredential: LocalAttributeDTO; + + beforeAll(async () => { + storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); }); - test("presentation", async () => { + test("standard presentation", async () => { const authorizationRequestUrl = ( await eudiploClient.createPresentationRequest({ responseType: "uri", @@ -169,43 +217,6 @@ describe("EUDIPLO", () => { const loadResult = await runtimeServices1.consumption.openId4Vc.resolveAuthorizationRequest({ authorizationRequestUrl }); const matchingCredentials = loadResult.value.matchingCredentials; - expect(matchingCredentials).toHaveLength(1); - - const queryResult = loadResult.value.authorizationRequest.dcql!.queryResult; - expect(queryResult.can_be_satisfied).toBe(true); - - const presentationResult = await runtimeServices1.consumption.openId4Vc.acceptAuthorizationRequest({ - authorizationRequest: loadResult.value.authorizationRequest, - attributeId: matchingCredentials[0].id - }); - expect(presentationResult).toBeSuccessful(); - expect(presentationResult.value.status).toBe(200); - }); - - // TODO: unskip once fix to CanCreateShareCredentialOffer has been deployed to the connector - // eslint-disable-next-line jest/no-disabled-tests - test.skip("issuance with request", async () => { - const oldCredentials = ( - await runtimeServices1.consumption.attributes.getAttributes({ - query: { - "content.value.@type": "VerifiableCredential" - } - }) - ).value; - - const sentMessage = ( - await serviceAxiosInstance.post("/enmeshed-demo/credential", { - recipient: runtimeServices1.address, - credentialConfigurationId: eudiploCredentialConfigurationId - }) - ).data.result; - - const requestId = (sentMessage.content as RequestJSON).id!; - await syncUntilHasMessageWithRequest(runtimeServices1.transport, requestId); - await runtimeServices1.consumption.incomingRequests.accept({ - requestId, - items: [{ accept: true }] - }); const currentCredentials = ( await runtimeServices1.consumption.attributes.getAttributes({ @@ -214,16 +225,17 @@ describe("EUDIPLO", () => { } }) ).value; - expect(currentCredentials).toHaveLength(oldCredentials.length + 1); + expect(matchingCredentials).toHaveLength(currentCredentials.length); - const oldCredentialIds = oldCredentials.map((c) => c.id); - const createdCredential = currentCredentials.find((c) => !oldCredentialIds.includes(c.id)); - expect(createdCredential).toBeDefined(); + const queryResult = loadResult.value.authorizationRequest.dcql!.queryResult; + expect(queryResult.can_be_satisfied).toBe(true); - const credentialContent = createdCredential!.content.value as VerifiableCredentialJSON; - const decodedCredential = decodeRecord(credentialContent.type, credentialContent.value) as SdJwtVcRecord; - expect(decodedCredential.firstCredential.prettyClaims.givenName).toBe("aGivenName"); - expect(credentialContent.value.split("~")).toHaveLength(3); // given name is selectively disclosable, hence length 3 + const presentationResult = await runtimeServices1.consumption.openId4Vc.acceptAuthorizationRequest({ + authorizationRequest: loadResult.value.authorizationRequest, + attributeId: matchingCredentials[0].id + }); + expect(presentationResult).toBeSuccessful(); + expect(presentationResult.value.status).toBe(200); }); test("presentation with request", async () => { @@ -256,8 +268,6 @@ describe("EUDIPLO", () => { describe("presentation token", () => { test("create presentation token", async () => { - const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); - const createPresentationTokenResult = await runtimeServices1.consumption.openId4Vc.createPresentationToken({ attributeId: storedCredential.id, expiresAt: CoreDate.utc().add({ minutes: 1 }).toString(), @@ -275,7 +285,6 @@ describe("EUDIPLO", () => { }); test("verify presentation token", async () => { - const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); const presentationToken = await createPresentationToken(storedCredential); const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ @@ -287,7 +296,6 @@ describe("EUDIPLO", () => { }); test("fail token verification in case of invalid nonce", async () => { - const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); const presentationToken = await createPresentationToken(storedCredential); const verificationResult = await runtimeServices1.consumption.openId4Vc.verifyPresentationToken({ @@ -300,7 +308,6 @@ describe("EUDIPLO", () => { }); test("fail token verification in case of invalid signature", async () => { - const storedCredential = await createAndStoreCredential(eudiploClient, eudiploCredentialConfigurationId); const presentationToken = await createPresentationToken(storedCredential); const tokenContentWithTamperedSignature = tamperSignatureOfTokenContent(presentationToken.content as TokenContentVerifiablePresentationJSON);