diff --git a/.dev/compose.openid4vc.yml b/.dev/compose.openid4vc.yml index 73906b76c..e404497b2 100644 --- a/.dev/compose.openid4vc.yml +++ b/.dev/compose.openid4vc.yml @@ -19,6 +19,8 @@ services: connector: image: ghcr.io/nmshd/connector:7.3.0-openid4vc.1@sha256:4be31417d10d67454d7732949601a2136417fefc78107e3751eccea7946a7aca + ports: + - "8080:80" environment: CUSTOM_CONFIG_LOCATION: "/config.json" transportLibrary__baseUrl: "http://consumer-api:8080" diff --git a/.dev/service-config.json b/.dev/service-config.json index b5757fa96..a88c2be62 100644 --- a/.dev/service-config.json +++ b/.dev/service-config.json @@ -19,5 +19,10 @@ }, "cors": { "origin": "*" + }, + "eudiplo": { + "baseUrl": "http://127.0.0.1:3000", + "user": "test-admin", + "password": "57c9cd444bf402b2cc1f5a0d2dafd3955bd9042c0372db17a4ede2d5fbda88e5" } } diff --git a/package-lock.json b/package-lock.json index fc28bcbbe..302d60f05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3405,6 +3405,57 @@ "resolved": "packages/app-runtime", "link": true }, + "node_modules/@nmshd/connector-sdk": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@nmshd/connector-sdk/-/connector-sdk-7.3.0.tgz", + "integrity": "sha512-Xj6n3/jIB3ssx1hKwjh0TBMbC+Bz18ytMB9OMtocudr8jv6EsoFgMI1/QLx10B6YK9ePOtY6WQBS+AkmYrKkNg==", + "dev": true, + "license": "AGPL-3.0-or-later", + "dependencies": { + "@nmshd/content": "7.2.0", + "@nmshd/runtime-types": "7.2.0", + "axios": "^1.13.2", + "form-data": "^4.0.5", + "qs": "^6.14.0" + } + }, + "node_modules/@nmshd/connector-sdk/node_modules/@nmshd/content": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@nmshd/content/-/content-7.2.0.tgz", + "integrity": "sha512-BXld/WcDaHuyFr5XfM3FVmx3T4wtMSbqBJbEqAvyFvVbLswGQtmZFJ+0U533UYZB2TWLX7ZzZCJhW7FpFkYWlQ==", + "dev": true, + "license": "AGPL-3.0-or-later", + "dependencies": { + "@js-soft/ts-serval": "2.0.14", + "@nmshd/core-types": "7.2.0", + "@nmshd/iql": "^1.0.4", + "ibantools": "^4.5.1", + "luxon": "^3.7.2", + "ts-simple-nameof": "^1.3.3" + } + }, + "node_modules/@nmshd/connector-sdk/node_modules/@nmshd/core-types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@nmshd/core-types/-/core-types-7.2.0.tgz", + "integrity": "sha512-rKUigSwD5jdg8ZznqmRMfBqf6LwxU+mrmMDuURa6TEq+dJQfr2jV1yaGlQaZ6ugy7hnwLHHTzBR5LF4zpBHU3Q==", + "dev": true, + "license": "AGPL-3.0-or-later", + "dependencies": { + "@js-soft/logging-abstractions": "^1.0.2", + "@js-soft/ts-serval": "2.0.14", + "@nmshd/crypto": "^2.1.3", + "json-stringify-safe": "^5.0.1", + "luxon": "^3.7.2", + "uuid": "^11.1.0" + } + }, + "node_modules/@nmshd/connector-sdk/node_modules/@nmshd/runtime-types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@nmshd/runtime-types/-/runtime-types-7.2.0.tgz", + "integrity": "sha512-YTTW7vN9CUmh7EWnl9uu+Nyd/LzjCukFQt+Bbaspd9PbdVJXA1q7YM6p7bwZ9TGW1tbeat245qpnP4a8d8LRbw==", + "dev": true, + "license": "AGPL-3.0-or-later" + }, "node_modules/@nmshd/consumption": { "resolved": "packages/consumption", "link": true @@ -16079,6 +16130,7 @@ "@js-soft/docdb-access-loki": "1.4.0", "@js-soft/docdb-access-mongo": "1.4.0", "@js-soft/node-logger": "1.2.1", + "@nmshd/connector-sdk": "^7.3.0", "@types/elliptic": "^6.4.18", "@types/json-stringify-safe": "^5.0.3", "@types/lodash": "^4.17.23", diff --git a/packages/consumption/src/modules/openid4vc/local/EnmeshedHolderKeyManagmentService.ts b/packages/consumption/src/modules/openid4vc/local/EnmeshedHolderKeyManagmentService.ts index beefbb096..1f96938ee 100644 --- a/packages/consumption/src/modules/openid4vc/local/EnmeshedHolderKeyManagmentService.ts +++ b/packages/consumption/src/modules/openid4vc/local/EnmeshedHolderKeyManagmentService.ts @@ -59,7 +59,7 @@ export class EnmshedHolderKeyManagmentService implements Kms.KeyManagementServic if (operation.operation === "deleteKey") { return true; } - if (operation.operation === "encrypt") { + if (operation.operation === "encrypt" && ["A128GCM", "A256GCM"].includes(operation.encryption.algorithm)) { return true; } return false; @@ -342,7 +342,7 @@ export class EnmshedHolderKeyManagmentService implements Kms.KeyManagementServic public async encrypt(agentContext: AgentContext, options: Kms.KmsEncryptOptions): Promise { try { - // encryption via A-256-GCM + // encryption via A-128-GCM/A-256-GCM // we will call the services side bob and the incoming side alice if (options.key.keyAgreement === undefined) { throw new Error("Key agreement is undefined"); @@ -351,11 +351,14 @@ export class EnmshedHolderKeyManagmentService implements Kms.KeyManagementServic throw new Error("Key agreement keyId is undefined"); } + const algorithm = options.encryption.algorithm; + const keyLength = options.encryption.algorithm === "A128GCM" ? 128 : 256; + // 1. derive the shared secret via ECDH-ES const sharedSecret = await this.ecdhEs(options.key.keyAgreement.keyId, options.key.keyAgreement.externalPublicJwk); agentContext.config.logger.debug(`EKM: Derived shared secret for encryption using ECDH-ES`); // 2. Concat KDF to form the final key - const derivedKey = this.concatKdf(sharedSecret, 256, "A256GCM", options.key.keyAgreement); + const derivedKey = this.concatKdf(sharedSecret, keyLength, algorithm, options.key.keyAgreement); // 3. Encrypt the data via AES-256-GCM using libsodium // create nonce diff --git a/packages/consumption/src/modules/requests/itemProcessors/shareAuthorizationRequest/ShareAuthorizationRequestRequestItemProcessor.ts b/packages/consumption/src/modules/requests/itemProcessors/shareAuthorizationRequest/ShareAuthorizationRequestRequestItemProcessor.ts index 53cdb5413..7ae879ab7 100644 --- a/packages/consumption/src/modules/requests/itemProcessors/shareAuthorizationRequest/ShareAuthorizationRequestRequestItemProcessor.ts +++ b/packages/consumption/src/modules/requests/itemProcessors/shareAuthorizationRequest/ShareAuthorizationRequestRequestItemProcessor.ts @@ -60,7 +60,10 @@ export class ShareAuthorizationRequestRequestItemProcessor extends GenericReques const attribute = (await this.consumptionController.attributes.getLocalAttribute(parsedParams.attributeId)) as OwnIdentityAttribute | undefined; if (!attribute) throw TransportCoreErrors.general.recordNotFound(LocalAttribute, parsedParams.attributeId.toString()); - await this.consumptionController.openId4Vc.acceptAuthorizationRequest(resolvedAuthorizationRequest.authorizationRequest, attribute); + const acceptResult = await this.consumptionController.openId4Vc.acceptAuthorizationRequest(resolvedAuthorizationRequest.authorizationRequest, attribute); + if (acceptResult.status !== 200) { + throw ConsumptionCoreErrors.requests.invalidAcceptParameters("The presentation was not successful. Try again later or select a different credential."); + } return AcceptResponseItem.from({ result: ResponseItemResult.Accepted }); } diff --git a/packages/runtime/package.json b/packages/runtime/package.json index bdc3e369b..5e49d1498 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -102,6 +102,7 @@ "@js-soft/docdb-access-loki": "1.4.0", "@js-soft/docdb-access-mongo": "1.4.0", "@js-soft/node-logger": "1.2.1", + "@nmshd/connector-sdk": "^7.3.0", "@types/elliptic": "^6.4.18", "@types/json-stringify-safe": "^5.0.3", "@types/lodash": "^4.17.23", diff --git a/packages/runtime/test/consumption/openid4vc.test.ts b/packages/runtime/test/consumption/openid4vc.test.ts index e2be95007..4b5f8c2e3 100644 --- a/packages/runtime/test/consumption/openid4vc.test.ts +++ b/packages/runtime/test/consumption/openid4vc.test.ts @@ -1,13 +1,34 @@ +import { ApiKeyAuthenticator, ConnectorClient } from "@nmshd/connector-sdk"; import { AcceptShareAuthorizationRequestRequestItemParametersJSON } from "@nmshd/consumption"; -import { RequestJSON, ResponseItemResult, VerifiableCredential, VerifiableCredentialJSON, VerifiablePresentationJSON } from "@nmshd/content"; +import { + RequestJSON, + ResponseItemResult, + ShareAuthorizationRequestRequestItemJSON, + VerifiableCredential, + VerifiableCredentialJSON, + VerifiablePresentationJSON +} from "@nmshd/content"; import axios, { AxiosInstance } from "axios"; import { jwtDecode } from "jwt-decode"; import * as client from "openid-client"; import path from "path"; -import { DockerComposeEnvironment, GenericContainer, StartedDockerComposeEnvironment, StartedTestContainer, Wait } from "testcontainers"; +import { DockerComposeEnvironment, StartedDockerComposeEnvironment, StartedTestContainer, Wait } from "testcontainers"; import { Agent as UndiciAgent, fetch as undiciFetch } from "undici"; -import { ShareCredentialOfferRequestItemProcessedByRecipientEvent } from "../../src"; -import { ensureActiveRelationship, exchangeAndAcceptRequestByMessage, exchangeMessageWithRequest, RuntimeServiceProvider, TestRuntimeServices } from "../lib"; +import { ShareCredentialOfferRequestItemProcessedByRecipientEvent, TransportServices } from "../../src"; +import { + emptyRelationshipCreationContent, + emptyRelationshipTemplateContent, + ensureActiveRelationship, + exchangeAndAcceptRequestByMessage, + exchangeMessageWithRequest, + RuntimeServiceProvider, + syncUntilHasMessageWithRequest, + syncUntilHasRelationships, + TestRuntimeServices +} from "../lib"; + +const connectorBaseUrl = "http://localhost:8080"; +const eudiploPort = 3000; // CAUTION: don't change this. The DCQL query has this port hardcoded in its configuration. The presentation test will fail if we change this. const fetchInstance: typeof fetch = (async (input: any, init: any) => { const response = await undiciFetch(input, { ...init, dispatcher: new UndiciAgent({}) }); @@ -51,50 +72,52 @@ const runtimeServiceProvider = new RuntimeServiceProvider(fetchInstance); let runtimeServices1: TestRuntimeServices; let runtimeServices2: TestRuntimeServices; +let serviceAxiosInstance: AxiosInstance; + +let dockerComposeStack: StartedDockerComposeEnvironment | undefined; + beforeAll(async () => { const runtimeServices = await runtimeServiceProvider.launch(2, { enableDeciderModule: true, enableRequestModule: true }); runtimeServices1 = runtimeServices[0]; runtimeServices2 = runtimeServices[1]; await ensureActiveRelationship(runtimeServices1.transport, runtimeServices2.transport); + + const connectorClient = ConnectorClient.create({ + baseUrl: connectorBaseUrl, + authenticator: new ApiKeyAuthenticator("aVeryCoolApiKeyWith30CharactersOr+") + }); + await createActiveRelationshipToConnector(runtimeServices1.transport, connectorClient); + + let oid4vcServiceBaseUrl = process.env.OPENID4VC_SERVICE_BASEURL!; + if (!oid4vcServiceBaseUrl) { + dockerComposeStack = await startOid4VcComposeStack(); + const mappedPort = dockerComposeStack.getContainer("oid4vc-service-1").getMappedPort(9000); + oid4vcServiceBaseUrl = `http://localhost:${mappedPort}`; + } + serviceAxiosInstance = axios.create({ + baseURL: oid4vcServiceBaseUrl, + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention + "Content-Type": "application/json" + } + }); }, 120000); afterAll(async () => { await runtimeServiceProvider.stop(); + + if (dockerComposeStack) await dockerComposeStack.down(); }); describe("custom openid4vc service", () => { - let axiosInstance: AxiosInstance; - let dockerComposeStack: StartedDockerComposeEnvironment | undefined; - - beforeAll(async () => { - let oid4vcServiceBaseUrl = process.env.OPENID4VC_SERVICE_BASEURL!; - if (!oid4vcServiceBaseUrl) { - dockerComposeStack = await startOid4VcComposeStack(); - const mappedPort = dockerComposeStack.getContainer("oid4vc-service-1").getMappedPort(9000); - oid4vcServiceBaseUrl = `http://localhost:${mappedPort}`; - } - - axiosInstance = axios.create({ - baseURL: oid4vcServiceBaseUrl, - headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention - "Content-Type": "application/json" - } - }); - }, 120000); - - afterAll(async () => { - if (dockerComposeStack) await dockerComposeStack.down(); - }); - let credentialOfferUrl: string; describe("sd-jwt", () => { let attributeId: string; test("should process a given sd-jwt credential offer", async () => { - const response = await axiosInstance.post("/issuance/credentialOffers", { + const response = await serviceAxiosInstance.post("/issuance/credentialOffers", { credentialConfigurationIds: ["EmployeeIdCard-sdjwt"] }); expect(response.status).toBe(200); @@ -126,7 +149,7 @@ describe("custom openid4vc service", () => { }); test("should be able to process a credential offer with pin authentication", async () => { - const response = await axiosInstance.post("/issuance/credentialOffers", { + const response = await serviceAxiosInstance.post("/issuance/credentialOffers", { credentialConfigurationIds: ["EmployeeIdCard-sdjwt"], authentication: "pin" }); @@ -164,7 +187,7 @@ describe("custom openid4vc service", () => { }); test("should be able to process a credential offer with external authentication", async () => { - const response = await axiosInstance.post("/issuance/credentialOffers", { + const response = await serviceAxiosInstance.post("/issuance/credentialOffers", { credentialConfigurationIds: ["EmployeeIdCard-sdjwt"], authentication: "externalAuthentication" @@ -219,7 +242,7 @@ describe("custom openid4vc service", () => { // Ensure the first test has completed expect(credentialOfferUrl).toBeDefined(); - const response = await axiosInstance.post("/presentation/presentationRequests", sdJwtPresentationRequestForPex); + const response = await serviceAxiosInstance.post("/presentation/presentationRequests", sdJwtPresentationRequestForPex); expect(response.status).toBe(200); const responseData = await response.data; @@ -242,7 +265,7 @@ describe("custom openid4vc service", () => { // Ensure the first test has completed expect(credentialOfferUrl).toBeDefined(); - const response = await axiosInstance.post("/presentation/presentationRequests", { + const response = await serviceAxiosInstance.post("/presentation/presentationRequests", { dcql: { credentials: [ { @@ -299,7 +322,7 @@ describe("custom openid4vc service", () => { expect(presentation.substring(0, credential.length)).toBe(credential); expect(presentation.substring(credential.length, credential.length + 2)).toBe("ey"); - const verificationResult = await axiosInstance.post("/presentation/verify", { + const verificationResult = await serviceAxiosInstance.post("/presentation/verify", { credential: presentation, format: "dc+sd-jwt", nonce: "defaultPresentationNonce", @@ -311,7 +334,7 @@ describe("custom openid4vc service", () => { describe("mdoc", () => { test("should process a given mdoc credential offer", async () => { - const response = await axiosInstance.post("/issuance/credentialOffers", { + const response = await serviceAxiosInstance.post("/issuance/credentialOffers", { credentialConfigurationIds: ["EmployeeIdCard-mdoc"] }); expect(response.status).toBe(200); @@ -345,7 +368,7 @@ describe("custom openid4vc service", () => { // Ensure the first test has completed expect(credentialOfferUrl).toBeDefined(); - const response = await axiosInstance.post("/presentation/presentationRequests", { + const response = await serviceAxiosInstance.post("/presentation/presentationRequests", { pex: { // see openid4vp-draft21.e2e.test.ts of credo for a more detailed example how to build a query id: "anId", @@ -402,7 +425,7 @@ describe("custom openid4vc service", () => { // Ensure the first test has completed expect(credentialOfferUrl).toBeDefined(); - const response = await axiosInstance.post("/presentation/presentationRequests", { + const response = await serviceAxiosInstance.post("/presentation/presentationRequests", { dcql: { credentials: [ { @@ -435,7 +458,7 @@ describe("custom openid4vc service", () => { }); test("transfer offer using requests", async () => { - const response = await axiosInstance.post("/issuance/credentialOffers", { + const response = await serviceAxiosInstance.post("/issuance/credentialOffers", { credentialConfigurationIds: ["EmployeeIdCard-sdjwt"] }); expect(response.status).toBe(200); @@ -467,7 +490,7 @@ describe("custom openid4vc service", () => { expect(attributes).toBeSuccessful(); expect(attributes.value.length).toBeGreaterThan(0); - const createPresentationResponse = await axiosInstance.post("/presentation/presentationRequests", sdJwtPresentationRequestForPex); + const createPresentationResponse = await serviceAxiosInstance.post("/presentation/presentationRequests", sdJwtPresentationRequestForPex); expect(createPresentationResponse.status).toBe(200); const createPresentationResponseData = await createPresentationResponse.data; @@ -490,7 +513,7 @@ describe("custom openid4vc service", () => { describe("request presentation using requests", () => { test("happy path", async () => { - const response = await axiosInstance.post("/presentation/presentationRequests", sdJwtPresentationRequestForPex); + const response = await serviceAxiosInstance.post("/presentation/presentationRequests", sdJwtPresentationRequestForPex); expect(response.status).toBe(200); const authorizationRequestUrl = response.data.result.presentationRequest as string; const authorizationRequestId = authorizationRequestUrl.split("%2F").at(-1)?.slice(0, 36); @@ -519,7 +542,7 @@ describe("custom openid4vc service", () => { const request = (await runtimeServices2.consumption.incomingRequests.getRequest({ id: requestId })).value; expect(request.response?.content.items[0]).toStrictEqual({ "@type": "AcceptResponseItem", result: ResponseItemResult.Accepted }); - const verificationStatus = (await axiosInstance.get(`/presentation/presentationRequests/${authorizationRequestId}/verificationSessionState`)).data.result; + const verificationStatus = (await serviceAxiosInstance.get(`/presentation/presentationRequests/${authorizationRequestId}/verificationSessionState`)).data.result; expect(verificationStatus).toBe("ResponseVerified"); }); @@ -535,7 +558,7 @@ describe("custom openid4vc service", () => { }) ).value.id; - const response = await axiosInstance.post("/presentation/presentationRequests", sdJwtPresentationRequestForPex); + const response = await serviceAxiosInstance.post("/presentation/presentationRequests", sdJwtPresentationRequestForPex); expect(response.status).toBe(200); const authorizationRequestUrl = response.data.result.presentationRequest as string; @@ -564,48 +587,19 @@ describe("custom openid4vc service", () => { expect(canAcceptResult.items[0].message).toBe("The credential selected for presentation doesn't match the query."); }); }); - - async function startOid4VcComposeStack() { - let baseUrl = process.env.NMSHD_TEST_BASEURL!; - let addressGenerationHostnameOverride: string | undefined; - - if (baseUrl.includes("localhost")) { - addressGenerationHostnameOverride = "localhost"; - baseUrl = baseUrl.replace("localhost", "host.docker.internal"); - } - - 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 - NMSHD_TEST_BASEURL: baseUrl, - - // eslint-disable-next-line @typescript-eslint/naming-convention - NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE: addressGenerationHostnameOverride - } as Record) - .withStartupTimeout(60000) - .withWaitStrategy("oid4vc-service", Wait.forHealthCheck()) - .up(); - - return composeStack; - } }); describe("EUDIPLO", () => { const eudiploUser = "test-admin"; - const eudiploPassword = "test"; - const eudiploIssuanceConfigurationId = "Employee ID Card"; - const eudiploPresentationConfigurationId = "Employee ID Card"; - const eudiploCredentialIdInConfiguration = "EmployeeIdCard"; - const eudiploPort = 3000; // CAUTION: don't change this. The DCQL query has this port hardcoded in its configuration. The presentation test will fail if we change this. + const eudiploPassword = "57c9cd444bf402b2cc1f5a0d2dafd3955bd9042c0372db17a4ede2d5fbda88e5"; + + const eudiploPresentationConfigurationId = "test"; + const eudiploCredentialConfigurationId = "test"; let eudiploContainer: StartedTestContainer | undefined; let axiosInstance: AxiosInstance; beforeAll(async () => { - eudiploContainer = await startEudiplo(); - const baseUrl = `http://localhost:${eudiploPort}`; const accessTokenResponse = await axios.post( @@ -641,9 +635,10 @@ describe("EUDIPLO", () => { test("issuance", async () => { const credentialOfferUrl = ( - await axiosInstance.post("/issuer-management/offer", { + await axiosInstance.post("/issuer/offer", { response_type: "uri", // eslint-disable-line @typescript-eslint/naming-convention - issuanceId: eudiploIssuanceConfigurationId + credentialConfigurationIds: [eudiploCredentialConfigurationId], + flow: "pre_authorized_code" }) ).data.uri; @@ -652,21 +647,19 @@ describe("EUDIPLO", () => { const credentialResponsesResult = await runtimeServices1.consumption.openId4Vc.requestCredentials({ credentialOffer: resolveCredentialOfferResult.value.credentialOffer, - credentialConfigurationIds: [eudiploCredentialIdInConfiguration] + 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].name).toBe("Employee ID Card"); + expect((storeCredentialsResponse.value.content.value as VerifiableCredentialJSON).displayInformation?.[0].name).toBe("test"); }); - // TODO: un-skip this test once a workable EUDIPLO version is available - the current version 1.9 doesn't work with credo because the exchange key for presentation encryption doesn't have a kid, and the currently latest version 1.13 can't be easily configured with the UI because the issuer display can't be configured - // eslint-disable-next-line jest/no-disabled-tests - test.skip("presentation", async () => { + test("presentation", async () => { const authorizationRequestUrl = ( - await axiosInstance.post(`/presentation-management/request`, { + await axiosInstance.post("/verifier/offer", { response_type: "uri", // eslint-disable-line @typescript-eslint/naming-convention requestId: eudiploPresentationConfigurationId }) @@ -687,24 +680,112 @@ describe("EUDIPLO", () => { expect(presentationResult.value.status).toBe(200); }); - function startEudiplo() { - const eudiploContainer = new GenericContainer("ghcr.io/openwallet-foundation-labs/eudiplo:1.9") - .withCopyDirectoriesToContainer([ - { - source: path.resolve(path.join(__dirname, "..", "..", "..", "..", ".dev", "eudiplo-assets")), - target: "/app/config" + test("issuance with request", async () => { + const oldCredentials = ( + await runtimeServices1.consumption.attributes.getAttributes({ + query: { + "content.value.@type": "VerifiableCredential" } - ]) - .withEnvironment({ - JWT_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 - PUBLIC_URL: `http://localhost:${eudiploPort}`, // eslint-disable-line @typescript-eslint/naming-convention - PORT: eudiploPort.toString() // eslint-disable-line @typescript-eslint/naming-convention }) - .withExposedPorts({ container: eudiploPort, host: eudiploPort }) - .start(); + ).value; - return eudiploContainer; - } + const sentMessage = ( + await serviceAxiosInstance.post("/enmeshed-demo/eudiplo/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 createdCredentials = ( + await runtimeServices1.consumption.attributes.getAttributes({ + query: { + "content.value.@type": "VerifiableCredential" + } + }) + ).value; + expect(createdCredentials).toHaveLength(oldCredentials.length + 1); + }); + + test("presentation with request", async () => { + const sentMessage = ( + await serviceAxiosInstance.post("/enmeshed-demo/eudiplo/presentation", { + recipient: runtimeServices1.address, + requestId: eudiploCredentialConfigurationId + }) + ).data.result; + + const requestId = (sentMessage.enmeshedMessage.content as RequestJSON).id!; + const receivedMessage = await syncUntilHasMessageWithRequest(runtimeServices1.transport, requestId); + + const matchingAttribute = ( + await runtimeServices1.consumption.openId4Vc.resolveAuthorizationRequest({ + authorizationRequestUrl: (receivedMessage.content.items[0] as ShareAuthorizationRequestRequestItemJSON).authorizationRequestUrl + }) + ).value.matchingCredentials[0]; + await runtimeServices1.consumption.incomingRequests.accept({ + requestId, + items: [{ accept: true, attributeId: matchingAttribute.id } as AcceptShareAuthorizationRequestRequestItemParametersJSON] + }); + + const sessionStatus = (await axiosInstance.get(`/session/${sentMessage.eudiploSessionId}`)).data.status; + expect(sessionStatus).toBe("completed"); + }); }); + +async function startOid4VcComposeStack() { + let baseUrl = process.env.NMSHD_TEST_BASEURL!; + let addressGenerationHostnameOverride: string | undefined; + + if (baseUrl.includes("localhost")) { + addressGenerationHostnameOverride = "localhost"; + baseUrl = baseUrl.replace("localhost", "host.docker.internal"); + } + + 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 + NMSHD_TEST_BASEURL: baseUrl, + + // eslint-disable-next-line @typescript-eslint/naming-convention + NMSHD_TEST_ADDRESSGENERATIONHOSTNAMEOVERRIDE: addressGenerationHostnameOverride, + JWT_SECRET: "OgwrDcgVQQ2yZwcFt7kPxQm3nUF+X3etF6MdLTstZAY=", // eslint-disable-line @typescript-eslint/naming-convention + AUTH_CLIENT_ID: "root", // eslint-disable-line @typescript-eslint/naming-convention + AUTH_CLIENT_SECRET: "root", // eslint-disable-line @typescript-eslint/naming-convention + PUBLIC_URL: `http://host.docker.internal:${eudiploPort}`, // eslint-disable-line @typescript-eslint/naming-convention + PORT: eudiploPort.toString() // eslint-disable-line @typescript-eslint/naming-convention + } as Record) + .withStartupTimeout(60000) + .withWaitStrategy("oid4vc-service", Wait.forHealthCheck()) + .up(); + + return composeStack; +} + +async function createActiveRelationshipToConnector(transport: TransportServices, connectorClient: ConnectorClient) { + const relationshipTemplate = ( + await connectorClient.relationshipTemplates.createOwnRelationshipTemplate({ + content: emptyRelationshipTemplateContent, + expiresAt: "2099" + }) + ).result; + + await transport.relationshipTemplates.loadPeerRelationshipTemplate({ reference: relationshipTemplate.reference.truncated }); + const relationshipId = ( + await transport.relationships.createRelationship({ + templateId: relationshipTemplate.id, + creationContent: emptyRelationshipCreationContent + }) + ).value.id; + + await connectorClient.relationships.acceptRelationship(relationshipId); + await syncUntilHasRelationships(transport); +}