diff --git a/packages/oid4vci-holder/CHANGELOG.md b/packages/oid4vci-holder/CHANGELOG.md new file mode 100644 index 000000000..420e6f23d --- /dev/null +++ b/packages/oid4vci-holder/CHANGELOG.md @@ -0,0 +1 @@ +# Change Log diff --git a/packages/oid4vci-holder/LICENSE b/packages/oid4vci-holder/LICENSE new file mode 100644 index 000000000..58f5f3bc2 --- /dev/null +++ b/packages/oid4vci-holder/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2024] [Sphereon BV] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/oid4vci-holder/README.md b/packages/oid4vci-holder/README.md new file mode 100644 index 000000000..3ffd7ad24 --- /dev/null +++ b/packages/oid4vci-holder/README.md @@ -0,0 +1,27 @@ + +

+
+ Sphereon +
OpenID for Verifiable Credentials Holder (OID4VCI) +
+

+ +--- + +**Warning: This package still is in very early development. Breaking changes without notice will happen at this point!** + +--- + +TODO + +## Installation + +```shell +yarn add @sphereon/ssi-sdk.oid4vci-holder +``` + +## Build + +```shell +yarn build +``` diff --git a/packages/oid4vci-holder/agent.yml b/packages/oid4vci-holder/agent.yml new file mode 100644 index 000000000..b5cf9c07a --- /dev/null +++ b/packages/oid4vci-holder/agent.yml @@ -0,0 +1,79 @@ +version: 3.0 + +constants: + baseUrl: http://localhost:3335 + port: 3335 + methods: + - oid4vciHolderGetMachineInterpreter + - oid4vciHolderGetInitiationData + - oid4vciHolderCreateCredentialSelection + - oid4vciHolderGetContact + - oid4vciHolderGetCredentials + - oid4vciHolderAddContactIdentity + - oid4vciHolderAssertValidCredentials + - oid4vciHolderStoreCredentialBranding + - oid4vciHolderStoreCredentials + +server: + baseUrl: + $ref: /constants/baseUrl + port: + $ref: /constants/port + use: + # CORS + - - $require: 'cors' + + # Add agent to the request object + - - $require: '@veramo/remote-server?t=function#RequestWithAgentRouter' + $args: + - agent: + $ref: /agent + + # API base path + - - /agent + - $require: '@veramo/remote-server?t=function#apiKeyAuth' + $args: + # Please configure your own API key. This is used when executing agent methods through ${baseUrl}/agent or ${baseUrl}/api-docs + - apiKey: test123 + - $require: '@veramo/remote-server?t=function#AgentRouter' + $args: + - exposedMethods: + $ref: /constants/methods + + # Open API schema + - - /open-api.json + - $require: '@veramo/remote-server?t=function#ApiSchemaRouter' + $args: + - basePath: :3335/agent + securityScheme: bearer + apiName: Agent + apiVersion: '1.0.0' + exposedMethods: + $ref: /constants/methods + + # Swagger docs + - - /api-docs + - $require: swagger-ui-express?t=object#serve + - $require: swagger-ui-express?t=function#setup + $args: + - null + - swaggerOptions: + url: '/open-api.json' + + # Execute during server initialization + init: + - $require: '@veramo/remote-server?t=function#createDefaultDid' + $args: + - agent: + $ref: /agent + baseUrl: + $ref: /constants/baseUrl + messagingServiceEndpoint: /messaging + +# Agent +agent: + $require: '@veramo/core#Agent' + $args: + - schemaValidation: false + plugins: + - $require: ./packages/oid4vci-holder/dist#OID4VCIHolder diff --git a/packages/oid4vci-holder/api-extractor.json b/packages/oid4vci-holder/api-extractor.json new file mode 100644 index 000000000..94c2c6a9f --- /dev/null +++ b/packages/oid4vci-holder/api-extractor.json @@ -0,0 +1,3 @@ +{ + "extends": "../include/api-extractor-base.json" +} diff --git a/packages/oid4vci-holder/package.json b/packages/oid4vci-holder/package.json new file mode 100644 index 000000000..47ede0467 --- /dev/null +++ b/packages/oid4vci-holder/package.json @@ -0,0 +1,59 @@ +{ + "name": "@sphereon/ssi-sdk.oid4vci-holder", + "version": "0.127.38-bram", + "source": "src/index.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "veramo": { + "pluginInterfaces": { + "IOID4VCIFlow": "./src/types/IOID4VCIFlow.ts" + } + }, + "scripts": { + "build": "tsc", + "build:clean": "tsc --build --clean && tsc --build" + }, + "dependencies": { + "@sphereon/ssi-sdk.data-store": "workspace:*", + "@sphereon/ssi-sdk.issuance-branding": "workspace:*", + "@sphereon/ssi-sdk.contact-manager": "workspace:*", + "@sphereon/ssi-types": "workspace:*", + "@sphereon/oid4vci-client": "0.9.1-next.2", + "@sphereon/oid4vci-common": "0.9.1-next.2", + "@veramo/utils": "4.2.0", + "@veramo/core": "4.2.0", + "@veramo/data-store": "4.2.0", + "xstate": "^4.38.3", + "uuid": "^9.0.1", + "i18n-js": "^3.8.0", + "lodash.memoize": "^4.1.2" + }, + "devDependencies": { + "typescript": "^4.9.5", + "typeorm": "0.3.12", + "@veramo/remote-client": "4.2.0", + "@veramo/remote-server": "4.2.0", + "@types/uuid": "^9.0.8", + "@types/i18n-js": "^3.8.4", + "@types/lodash.memoize": "^4.1.7" + }, + "files": [ + "dist/**/*", + "src/**/*", + "README.md", + "plugin.schema.json", + "LICENSE" + ], + "private": false, + "publishConfig": { + "access": "public" + }, + "repository": "git@github.com:Sphereon-Opensource/SSI-SDK.git", + "author": "Sphereon ", + "license": "Apache-2.0", + "keywords": [ + "Sphereon", + "OID4VCI", + "State Machine" + ] +} diff --git a/packages/oid4vci-holder/sphereon-ssi-sdk.oid4vci-holder-0.127.38-bram.tgz b/packages/oid4vci-holder/sphereon-ssi-sdk.oid4vci-holder-0.127.38-bram.tgz new file mode 100644 index 000000000..34b0ba9ed Binary files /dev/null and b/packages/oid4vci-holder/sphereon-ssi-sdk.oid4vci-holder-0.127.38-bram.tgz differ diff --git a/packages/oid4vci-holder/src/agent/OID4VCIHolder.ts b/packages/oid4vci-holder/src/agent/OID4VCIHolder.ts new file mode 100644 index 000000000..95df997bc --- /dev/null +++ b/packages/oid4vci-holder/src/agent/OID4VCIHolder.ts @@ -0,0 +1,291 @@ +import { OpenID4VCIClient } from '@sphereon/oid4vci-client' +import { CredentialSupported, DefaultURISchemes } from '@sphereon/oid4vci-common' +import { + CorrelationIdentifierEnum, + IBasicCredentialLocaleBranding, + Identity, + IdentityRoleEnum, + NonPersistedIdentity, + Party, +} from '@sphereon/ssi-sdk.data-store' +import { IAgentPlugin, VerifiableCredential } from '@veramo/core' +import { computeEntryHash } from '@veramo/utils' +import { v4 as uuidv4 } from 'uuid' +import { OID4VCIMachine } from '../machine/oid4vciMachine' +import { + getCredentialBranding, + getSupportedCredentials, + mapCredentialToAccept, + selectCredentialLocaleBranding, + verifyCredentialToAccept, +} from './OID4VCIHolderService' +import { + AddContactIdentityArgs, + AssertValidCredentialsArgs, + CreateCredentialSelectionArgs, + CredentialToAccept, + CredentialTypeSelection, + GetContactArgs, + GetCredentialsArgs, + OnCredentialsArgs, + IOID4VCIHolder, + InitiateOID4VCIArgs, + InitiationData, + MappedCredentialToAccept, + OID4VCIHolderEvent, + OID4VCIHolderOptions, + OID4VCIMachineInstanceOpts, + OID4VCIMachineInterpreter, + OnContactIdentityCreatedArgs, + OnCredentialStoredArgs, + RequestType, + RequiredContext, + StoreCredentialBrandingArgs, + StoreCredentialsArgs, +} from '../types/IOID4VCIHolder' + +/** + * {@inheritDoc IOID4VCIHolder} + */ + +export class OID4VCIHolder implements IAgentPlugin { + readonly eventTypes: Array = [ + OID4VCIHolderEvent.CONTACT_IDENTITY_CREATED, + OID4VCIHolderEvent.CREDENTIAL_STORED + ] + + readonly methods: IOID4VCIHolder = { + oid4vciHolderGetMachineInterpreter: this.oid4vciHolderGetMachineInterpreter.bind(this), + oid4vciHolderGetInitiationData: this.oid4vciHolderGetInitiationData.bind(this), + oid4vciHolderCreateCredentialSelection: this.oid4vciHolderCreateCredentialSelection.bind(this), + oid4vciHolderGetContact: this.oid4vciHolderGetContact.bind(this), + oid4vciHolderGetCredentials: this.oid4vciHolderGetCredentials.bind(this), + oid4vciHolderAddContactIdentity: this.oid4vciHolderAddContactIdentity.bind(this), + oid4vciHolderAssertValidCredentials: this.oid4vciHolderAssertValidCredentials.bind(this), + oid4vciHolderStoreCredentialBranding: this.oid4vciHolderStoreCredentialBranding.bind(this), + oid4vciHolderStoreCredentials: this.oid4vciHolderStoreCredentials.bind(this), + } + + private readonly vcFormatPreferences: Array = ['jwt_vc_json', 'jwt_vc', 'ldp_vc'] + private readonly onContactIdentityCreated?: (args: OnContactIdentityCreatedArgs) => Promise + private readonly onCredentialStored?: (args: OnCredentialStoredArgs) => Promise + private readonly onGetCredentials: (args: OnCredentialsArgs) => Promise> + + constructor(options: OID4VCIHolderOptions) { + const { + onContactIdentityCreated, + onCredentialStored, + onGetCredentials, vcFormatPreferences + } = options + + if (vcFormatPreferences !== undefined && vcFormatPreferences.length > 0) { + this.vcFormatPreferences = vcFormatPreferences + } + this.onGetCredentials = onGetCredentials + this.onContactIdentityCreated = onContactIdentityCreated + this.onCredentialStored = onCredentialStored + } + + public async onEvent(event: any, context: RequiredContext): Promise { + switch (event.type) { + case OID4VCIHolderEvent.CONTACT_IDENTITY_CREATED: + this.onContactIdentityCreated?.(event.data) + break + case OID4VCIHolderEvent.CREDENTIAL_STORED: + this.onCredentialStored?.(event.data) + break + default: + return Promise.reject(Error('Event type not supported')) + } + } + + private async oid4vciHolderGetMachineInterpreter(args: OID4VCIMachineInstanceOpts, context: RequiredContext): Promise { + const services = { + initiateOID4VCI: (args: InitiateOID4VCIArgs) => this.oid4vciHolderGetInitiationData(args, context), + createCredentialSelection: (args: CreateCredentialSelectionArgs) => this.oid4vciHolderCreateCredentialSelection(args, context), + getContact: (args: GetContactArgs) => this.oid4vciHolderGetContact(args, context), + getCredentials: (args: GetCredentialsArgs) => this.oid4vciHolderGetCredentials(args, context), + addContactIdentity: (args: AddContactIdentityArgs) => this.oid4vciHolderAddContactIdentity(args, context), + assertValidCredentials: (args: AssertValidCredentialsArgs) => this.oid4vciHolderAssertValidCredentials(args, context), + storeCredentialBranding: (args: StoreCredentialBrandingArgs) => this.oid4vciHolderStoreCredentialBranding(args, context), + storeCredentials: (args: StoreCredentialsArgs) => this.oid4vciHolderStoreCredentials(args, context), + } + + const oid4vciMachineInstanceArgs: OID4VCIMachineInstanceOpts = { + ...args, + services: { + ...services, + ...args.services, + } + } + + return OID4VCIMachine.newInstance(oid4vciMachineInstanceArgs) + } + + private async oid4vciHolderGetInitiationData(args: InitiateOID4VCIArgs, context: RequiredContext): Promise { + const { requestData } = args + + if (requestData?.uri === undefined) { + return Promise.reject(Error('Missing request uri in context')) + } + + if (!requestData?.uri || !(requestData?.uri.startsWith(RequestType.OPENID_INITIATE_ISSUANCE) || requestData?.uri.startsWith(RequestType.OPENID_CREDENTIAL_OFFER))) { + return Promise.reject(Error('Invalid Uri')) + } + + const openID4VCIClient = await OpenID4VCIClient.fromURI({ + uri: requestData?.uri, + authorizationRequest: {redirectUri: `${DefaultURISchemes.CREDENTIAL_OFFER}://`}, + }) + + const serverMetadata = await openID4VCIClient.retrieveServerMetadata() + const credentialsSupported = await getSupportedCredentials({ openID4VCIClient, vcFormatPreferences: this.vcFormatPreferences }) + const credentialBranding = await getCredentialBranding({ credentialsSupported, context }) + const authorizationCodeURL = openID4VCIClient.authorizationURL + const openID4VCIClientState = JSON.parse(await openID4VCIClient.exportState()) + + return { + authorizationCodeURL, + credentialBranding, + credentialsSupported, + serverMetadata, + openID4VCIClientState, + } + } + + private async oid4vciHolderCreateCredentialSelection(args: CreateCredentialSelectionArgs, context: RequiredContext): Promise> { + const { credentialsSupported, credentialBranding, locale, selectedCredentials } = args + const credentialSelection: Array = await Promise.all( + credentialsSupported.map(async (credentialMetadata: CredentialSupported): Promise => { + if (!('types' in credentialMetadata)) { + return Promise.reject(Error('SD-JWT not supported yet')) + } + // FIXME this allows for duplicate VerifiableCredential, which the user has no idea which ones those are and we also have a branding map with unique keys, so some branding will not match + const defaultCredentialType = 'VerifiableCredential' + const credentialType = credentialMetadata.types.find((type: string): boolean => type !== defaultCredentialType) ?? defaultCredentialType + const localeBranding = credentialBranding?.get(credentialType) + const credentialAlias = (await selectCredentialLocaleBranding({ locale, localeBranding }))?.alias + + return { + id: uuidv4(), + credentialType, + credentialAlias: credentialAlias ?? credentialType, + isSelected: false, + } + }) + ) + + // TODO find better place to do this, would be nice if the machine does this? + if (credentialSelection.length === 1) { + selectedCredentials.push(credentialSelection[0].credentialType) + } + + return credentialSelection + } + + private async oid4vciHolderGetContact(args: GetContactArgs, context: RequiredContext): Promise { + const { serverMetadata } = args + + if (serverMetadata === undefined) { + return Promise.reject(Error('Missing serverMetadata in context')) + } + + const correlationId: string = new URL(serverMetadata.issuer).hostname + return context.agent + .cmGetContacts({ + filter: [ + { + identities: { + identifier: { + correlationId, + }, + }, + }, + ], + }) + .then((contacts: Array): Party | undefined => (contacts.length === 1 ? contacts[0] : undefined)) + } + + private async oid4vciHolderGetCredentials(args: GetCredentialsArgs, context: RequiredContext): Promise> { + const { verificationCode, selectedCredentials, openID4VCIClientState } = args + + if (!openID4VCIClientState) { + throw Error('Missing openID4VCI client state in context') + } + + return this.onGetCredentials({ + credentials: selectedCredentials, + pin: verificationCode, + openID4VCIClientState, + }) + .then((credentials: Array) => mapCredentialToAccept({ credentials })) + } + + private async oid4vciHolderAddContactIdentity(args: AddContactIdentityArgs, context: RequiredContext): Promise { + const { credentialsToAccept, contact } = args + + if (!contact) { + return Promise.reject(Error('Missing contact in context')) + } + + if (credentialsToAccept === undefined || credentialsToAccept.length === 0) { + return Promise.reject(Error('Missing credential offers in context')) + } + + const correlationId: string = credentialsToAccept[0].correlationId + const identity: NonPersistedIdentity = { + alias: correlationId, + roles: [IdentityRoleEnum.ISSUER], + identifier: { + type: CorrelationIdentifierEnum.DID, + correlationId, + }, + } + + await context.agent.emit(OID4VCIHolderEvent.CONTACT_IDENTITY_CREATED, { + contactId: contact.id, + identity + }) + + return context.agent.cmAddIdentity({ contactId: contact.id, identity }) + } + + private async oid4vciHolderAssertValidCredentials(args: AssertValidCredentialsArgs, context: RequiredContext): Promise { + const { credentialsToAccept } = args + + await Promise.all( + credentialsToAccept.map( + async (mappedCredential: MappedCredentialToAccept): Promise => verifyCredentialToAccept({ mappedCredential, context }) + ) + ) + } + + private async oid4vciHolderStoreCredentialBranding(args: StoreCredentialBrandingArgs, context: RequiredContext): Promise { + const { credentialBranding, serverMetadata, selectedCredentials, credentialsToAccept } = args + + if (serverMetadata === undefined) { + return Promise.reject(Error('Missing serverMetadata in context')) + } + + const localeBranding: Array | undefined = credentialBranding?.get(selectedCredentials[0]) + if (localeBranding && localeBranding.length > 0) { + await context.agent.addCredentialBranding({ + vcHash: computeEntryHash(credentialsToAccept[0].rawVerifiableCredential), + issuerCorrelationId: new URL(serverMetadata.issuer).hostname, + localeBranding, + }) + } + } + + private async oid4vciHolderStoreCredentials(args: StoreCredentialsArgs, context: RequiredContext): Promise { + const { credentialsToAccept } = args + + const verifiableCredential: VerifiableCredential = credentialsToAccept[0].rawVerifiableCredential + const vcHash = await context.agent.dataStoreSaveVerifiableCredential({ verifiableCredential }) + + await context.agent.emit(OID4VCIHolderEvent.CREDENTIAL_STORED, { + vcHash, + credential: verifiableCredential + }) + } +} diff --git a/packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts b/packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts new file mode 100644 index 000000000..c5e4b9a11 --- /dev/null +++ b/packages/oid4vci-holder/src/agent/OID4VCIHolderService.ts @@ -0,0 +1,228 @@ +import { + CredentialOfferFormat, + CredentialResponse, + CredentialsSupportedDisplay, + CredentialSupported, + OpenId4VCIVersion +} from '@sphereon/oid4vci-common' +import { + CredentialToAccept, + GetCredentialBrandingArgs, + GetPreferredCredentialFormatsArgs, + GetSupportedCredentialsArgs, + MapCredentialToAcceptArgs, + MappedCredentialToAccept, + RequiredContext, + SelectAppLocaleBrandingArgs, + VerificationResult, + VerificationSubResult, + VerifyCredentialToAcceptArgs +} from '../types/IOID4VCIHolder' +import { IBasicCredentialLocaleBranding, IBasicIssuerLocaleBranding } from '@sphereon/ssi-sdk.data-store' +import { credentialLocaleBrandingFrom } from './OIDC4VCIBrandingMapper' +import { + CredentialMapper, + IVerifiableCredential, + IVerifyResult, + OriginalVerifiableCredential, + W3CVerifiableCredential, + WrappedVerifiableCredential, +} from '@sphereon/ssi-types' +import { IVerifyCredentialArgs, VerifiableCredential } from '@veramo/core' +import { translate } from '../localization/Localization' + +export const getSupportedCredentials = async (args: GetSupportedCredentialsArgs): Promise> => { + const { openID4VCIClient, vcFormatPreferences } = args + + if (!openID4VCIClient.credentialOffer) { + return Promise.reject(Error('openID4VCIClient has no credentialOffer')) + } + + // todo: remove format here. This is just a temp hack for V11+ issuance of only one credential. Having a single array with formats for multiple credentials will not work. This should be handled in VCI itself + let format: string[] | undefined = undefined + if (openID4VCIClient.version() > OpenId4VCIVersion.VER_1_0_09 && typeof openID4VCIClient.credentialOffer.credential_offer === 'object') { + format = openID4VCIClient.credentialOffer.credential_offer.credentials + .filter((format: string | CredentialOfferFormat): boolean => typeof format !== 'string') + .map((format: string | CredentialOfferFormat) => (format as CredentialOfferFormat).format) + if (format?.length === 0) { + format = undefined // Otherwise we would match nothing + } + } + + const credentialsSupported: Array = openID4VCIClient.getCredentialsSupported(true, format) + return getPreferredCredentialFormats({ credentials: credentialsSupported, vcFormatPreferences }) +} + +export const getCredentialBranding = async (args: GetCredentialBrandingArgs): Promise>> => { + const { credentialsSupported, context } = args + const credentialBranding = new Map>() + await Promise.all( + credentialsSupported.map(async (credential: CredentialSupported): Promise => { + const localeBranding: Array = await Promise.all( + (credential.display ?? []).map( + async (display: CredentialsSupportedDisplay): Promise => + await context.agent.ibCredentialLocaleBrandingFrom({ localeBranding: await credentialLocaleBrandingFrom(display) }) + ) + ) + + const defaultCredentialType = 'VerifiableCredential' + const credentialTypes: Array = + // @ts-ignore + credential.types.length > 1 + // @ts-ignore + ? credential.types.filter((type: string): boolean => type !== defaultCredentialType) + // @ts-ignore + : credential.types.length === 0 + ? [defaultCredentialType] + // @ts-ignore + : credential.types + + credentialBranding.set(credentialTypes[0], localeBranding) // TODO for now taking the first type + }) + ) + + return credentialBranding +} + +export const getPreferredCredentialFormats = async (args: GetPreferredCredentialFormatsArgs): Promise> => { + const { credentials, vcFormatPreferences } = args + // Group credentials based on types as we now have multiple entries for one vc with different formats + const groupedTypes: Array = Array.from( + // TODO any + credentials + // @ts-ignore + .reduce((map: Map, value: CredentialSupported) => map.set(value.types.toString(), [...(map.get(value.types.toString()) || []), value]), new Map()) + .values() + ) + + const preferredCredentials: Array = [] + + for (const group of groupedTypes) { + for (const vcFormatPreference of vcFormatPreferences) { + const credentialSupported = group.find((credentialSupported: CredentialSupported): boolean => credentialSupported.format === vcFormatPreference) + if (credentialSupported) { + preferredCredentials.push(credentialSupported) + break + } + } + } + + return preferredCredentials +} + +export const selectCredentialLocaleBranding = (args: SelectAppLocaleBrandingArgs): Promise => { + const { locale, localeBranding } = args + + const branding = localeBranding?.find( + (branding: IBasicCredentialLocaleBranding | IBasicIssuerLocaleBranding) => + locale ? branding.locale?.startsWith(locale) || branding.locale === undefined : branding.locale === undefined // TODO refactor as we duplicate code + ) + + // FIXME as we should be able to just return the value directly + return Promise.resolve(branding) +} + +export const verifyCredentialToAccept = async (args: VerifyCredentialToAcceptArgs): Promise => { + const { mappedCredential, context } = args + + const credential = mappedCredential.credential.credentialResponse.credential as OriginalVerifiableCredential + const wrappedVC = CredentialMapper.toWrappedVerifiableCredential(credential) + if ( + wrappedVC.decoded?.iss?.includes('did:ebsi:') || + (typeof wrappedVC.decoded?.vc?.issuer === 'string' + ? wrappedVC.decoded?.vc?.issuer?.includes('did:ebsi:') + : wrappedVC.decoded?.vc?.issuer?.id?.includes('did:ebsi:')) + ) { + // TODO: Skipping VC validation for EBSI conformance issued credential, as their Issuer is not present in the ledger (sigh) + if (JSON.stringify(wrappedVC.decoded).includes('vc:ebsi:conformance')) { + return + } + } + + const verificationResult: VerificationResult = await verifyCredential( + { + credential: credential as VerifiableCredential, + // TODO WAL-675 we might want to allow these types of options as part of the context, now we have state machines. Allows us to pre-determine whether these policies apply and whether remote context should be fetched + fetchRemoteContexts: true, + policies: { + credentialStatus: false, + expirationDate: false, + issuanceDate: false, + }, + }, + context + ) + + if (!verificationResult.result || verificationResult.error) { + return Promise.reject( + Error(verificationResult.result ? verificationResult.error : translate('oid4vci_machine_credential_verification_failed_message')) + ) + } +} + +// TODO, refactor +export const verifyCredential = async (args: IVerifyCredentialArgs, context: RequiredContext): Promise => { + // We also allow/add boolean, because 4.x Veramo returns a boolean for JWTs. 5.X will return better results + const result: IVerifyResult | boolean = (await context.agent.verifyCredential(args)) as IVerifyResult | boolean + + if (typeof result === 'boolean') { + return { + source: CredentialMapper.toWrappedVerifiableCredential(args.credential as OriginalVerifiableCredential), + result, + ...(!result && { + error: 'Invalid JWT VC', + errorDetails: `JWT VC could was not valid with policies: ${JSON.stringify(args.policies)}`, + }), + subResults: [], + } + } else { + const subResults: Array = [] + let error: string | undefined + let errorDetails: string | undefined + if (result.error) { + error = result.error?.message ?? '' + errorDetails = result.error?.details?.code ?? '' + errorDetails = (errorDetails !== '' ? `${errorDetails}, ` : '') + (result.error?.details?.url ?? '') + if (result.error?.errors) { + error = (error !== '' ? `${error}, ` : '') + result.error?.errors?.map((error) => error.message ?? error.name).join(', ') + errorDetails = + (errorDetails !== '' ? `${errorDetails}, ` : '') + + result.error?.errors?.map((error) => (error?.details?.code ? `${error.details.code}, ` : '') + (error?.details?.url ?? '')).join(', ') + } + } + + return { + source: CredentialMapper.toWrappedVerifiableCredential(args.credential as OriginalVerifiableCredential), + result: result.verified, + subResults, + error, + errorDetails, + } + } +} + +export const mapCredentialToAccept = async (args: MapCredentialToAcceptArgs): Promise> => { + const { credentials } = args + return credentials.map((credential: CredentialToAccept): MappedCredentialToAccept => { + const credentialResponse: CredentialResponse = credential.credentialResponse + const verifiableCredential: W3CVerifiableCredential | undefined = credentialResponse.credential + const wrappedVerifiableCredential: WrappedVerifiableCredential = CredentialMapper.toWrappedVerifiableCredential( + verifiableCredential as OriginalVerifiableCredential + ) + if (wrappedVerifiableCredential?.credential?.compactSdJwtVc) { + throw Error('SD-JWT not supported yet') + } + const uniformVerifiableCredential: IVerifiableCredential = wrappedVerifiableCredential.credential + const rawVerifiableCredential: VerifiableCredential = credentialResponse.credential as unknown as VerifiableCredential + + const correlationId: string = + typeof uniformVerifiableCredential.issuer === 'string' ? uniformVerifiableCredential.issuer : uniformVerifiableCredential.issuer.id + + return { + correlationId, + credential: credential, + rawVerifiableCredential, + uniformVerifiableCredential, + } + }) +} diff --git a/packages/oid4vci-holder/src/agent/OIDC4VCIBrandingMapper.ts b/packages/oid4vci-holder/src/agent/OIDC4VCIBrandingMapper.ts new file mode 100644 index 000000000..e2eed2e1d --- /dev/null +++ b/packages/oid4vci-holder/src/agent/OIDC4VCIBrandingMapper.ts @@ -0,0 +1,49 @@ +import { CredentialsSupportedDisplay } from '@sphereon/oid4vci-common' +import { IBasicCredentialLocaleBranding } from '@sphereon/ssi-sdk.data-store' + +export const credentialLocaleBrandingFrom = async (credentialDisplay: CredentialsSupportedDisplay): Promise => { + return { + ...(credentialDisplay.name && { + alias: credentialDisplay.name, + }), + ...(credentialDisplay.locale && { + locale: credentialDisplay.locale, + }), + ...(credentialDisplay.logo && { + logo: { + ...(credentialDisplay.logo.url && { + uri: credentialDisplay.logo?.url, + }), + ...(credentialDisplay.logo.alt_text && { + alt: credentialDisplay.logo?.alt_text, + }), + }, + }), + ...(credentialDisplay.description && { + description: credentialDisplay.description, + }), + + ...(credentialDisplay.text_color && { + text: { + color: credentialDisplay.text_color, + }, + }), + ...((credentialDisplay.background_image || credentialDisplay.background_color) && { + background: { + ...(credentialDisplay.background_image && { + image: { + ...(credentialDisplay.background_image.url && { + uri: credentialDisplay.background_image?.url, + }), + ...(credentialDisplay.background_image.alt_text && { + alt: credentialDisplay.background_image?.alt_text, + }), + }, + }), + ...(credentialDisplay.background_color && { + color: credentialDisplay.background_color, + }), + }, + }), + } +} diff --git a/packages/oid4vci-holder/src/index.ts b/packages/oid4vci-holder/src/index.ts new file mode 100644 index 000000000..e3a9a9fc1 --- /dev/null +++ b/packages/oid4vci-holder/src/index.ts @@ -0,0 +1,6 @@ +/** + * @public + */ + +export { OID4VCIHolder } from './agent/OID4VCIHolder' +export * from './types/IOID4VCIHolder' diff --git a/packages/oid4vci-holder/src/localization/Localization.ts b/packages/oid4vci-holder/src/localization/Localization.ts new file mode 100644 index 000000000..81be7ebec --- /dev/null +++ b/packages/oid4vci-holder/src/localization/Localization.ts @@ -0,0 +1,49 @@ +import i18n, { Scope, TranslateOptions } from 'i18n-js' +import memoize from 'lodash.memoize' +import { SupportedLanguage } from '../types/IOID4VCIHolder' + +class Localization { + private static translationGetters: { [locale: string]: () => object } = { + [SupportedLanguage.ENGLISH]: () => require('./translations/en.json'), + [SupportedLanguage.DUTCH]: () => require('./translations/nl.json'), + } + + public static translate: any = memoize( + (key: Scope, config?: TranslateOptions) => { + // If no LocaleProvider is used we need to load the default locale as the translations will be empty + if (Object.keys(i18n.translations).length === 0) { + i18n.translations = { + [SupportedLanguage.ENGLISH]: Localization.translationGetters[SupportedLanguage.ENGLISH](), + } + i18n.locale = SupportedLanguage.ENGLISH + } else { + i18n.translations = { + [i18n.locale]: { + ...i18n.translations[i18n.locale], + ...Localization.translationGetters[this.findSupportedLanguage(i18n.locale) || SupportedLanguage.ENGLISH](), + }, + } + } + + return i18n.t(key, config) + }, + (key: Scope, config?: TranslateOptions) => (config ? key + JSON.stringify(config) : key) + ) + + private static findSupportedLanguage = (locale: string): string | undefined => { + for (const language of Object.values(SupportedLanguage)) { + if (language === locale) { + return language + } + } + + return undefined + } + + public static getLocale = (): string => { + return i18n.locale || SupportedLanguage.ENGLISH + } +} + +export const translate = Localization.translate +export default Localization diff --git a/packages/oid4vci-holder/src/localization/translations/en.json b/packages/oid4vci-holder/src/localization/translations/en.json new file mode 100644 index 000000000..b90ed8fe1 --- /dev/null +++ b/packages/oid4vci-holder/src/localization/translations/en.json @@ -0,0 +1,11 @@ +{ + "oid4vci_machine_verify_credentials_error_title": "Verify credentials", + "oid4vci_machine_store_credential_branding_error_title": "Store credential branding", + "oid4vci_machine_store_credential_error_title": "Store credential", + "oid4vci_machine_add_contact_identity_error_title": "Add contact identity", + "oid4vci_machine_retrieve_credentials_error_title": "Retrieve credentials", + "oid4vci_machine_retrieve_contact_error_title": "Retrieve contact", + "oid4vci_machine_credential_selection_error_title": "Credential selection", + "oid4vci_machine_initiation_error_title": "Initiate OID4VCI provider", + "oid4vci_machine_credential_verification_failed_message": "The credential verification resulted in an error." +} diff --git a/packages/oid4vci-holder/src/localization/translations/nl.json b/packages/oid4vci-holder/src/localization/translations/nl.json new file mode 100644 index 000000000..6aa01575a --- /dev/null +++ b/packages/oid4vci-holder/src/localization/translations/nl.json @@ -0,0 +1,11 @@ +{ + "oid4vci_machine_verify_credentials_error_title": "Verifiëren credential", + "oid4vci_machine_store_credential_branding_error_title": "Opslaan credential branding", + "oid4vci_machine_store_credential_error_title": "Opslaan credential", + "oid4vci_machine_add_contact_identity_error_title": "Toevoegen identiteit contact", + "oid4vci_machine_retrieve_credentials_error_title": "Ophalen credential", + "oid4vci_machine_retrieve_contact_error_title": "Ophalen contact", + "oid4vci_machine_credential_selection_error_title": "Credential selectie", + "oid4vci_machine_initiation_error_title": "Initiëren OID4VCI provider", + "oid4vci_machine_credential_verification_failed_message": "Verificatie van de credential leidde tot een fout." +} diff --git a/packages/oid4vci-holder/src/machine/oid4vciMachine.ts b/packages/oid4vci-holder/src/machine/oid4vciMachine.ts new file mode 100644 index 000000000..b38a05610 --- /dev/null +++ b/packages/oid4vci-holder/src/machine/oid4vciMachine.ts @@ -0,0 +1,564 @@ +import { AuthzFlowType, toAuthorizationResponsePayload } from '@sphereon/oid4vci-common' +import { Party, Identity } from '@sphereon/ssi-sdk.data-store' +import { v4 as uuidv4 } from 'uuid' +import { assign, createMachine, DoneInvokeEvent, interpret } from 'xstate' +import { + ContactAliasEvent, + ContactConsentEvent, + CreateContactEvent, + CreateOID4VCIMachineOpts, + ErrorDetails, + CredentialTypeSelection, + MappedCredentialToAccept, + OID4VCIMachineAddContactStates, + OID4VCIMachineContext, + OID4VCIMachineEvents, + OID4VCIMachineEventTypes, + OID4VCIMachineGuards, + OID4VCIMachineInstanceOpts, + OID4VCIMachineInterpreter, + OID4VCIMachineServices, + OID4VCIMachineState, + OID4VCIMachineStates, + OID4VCIMachineVerifyPinStates, + OID4VCIStateMachine, + SelectCredentialsEvent, + VerificationCodeEvent, + InitiationData, + SetAuthorizationCodeURLEvent, + AuthorizationResponseEvent, +} from '../types/IOID4VCIHolder' +import { translate } from '../localization/Localization' + +const oid4vciHasNoContactGuard = (_ctx: OID4VCIMachineContext, _event: OID4VCIMachineEventTypes): boolean => { + const { contact } = _ctx + return contact === undefined +} + +const oid4vciHasContactGuard = (_ctx: OID4VCIMachineContext, _event: OID4VCIMachineEventTypes): boolean => { + const { contact } = _ctx + return contact !== undefined +} + +const oid4vciSelectCredentialsGuard = (_ctx: OID4VCIMachineContext, _event: OID4VCIMachineEventTypes): boolean => { + const { credentialSelection } = _ctx + return credentialSelection.length > 1 +} + +const oid4vciRequirePinGuard = (_ctx: OID4VCIMachineContext, _event: OID4VCIMachineEventTypes): boolean => { + const { requestData } = _ctx + return requestData?.credentialOffer.userPinRequired === true +} + +const oid4vciHasNoContactIdentityGuard = (_ctx: OID4VCIMachineContext, _event: OID4VCIMachineEventTypes): boolean => { + const { contact, credentialsToAccept } = _ctx + return !contact?.identities!.some((identity: Identity): boolean => identity.identifier.correlationId === credentialsToAccept[0].correlationId) +} + +const oid4vciVerificationCodeGuard = (_ctx: OID4VCIMachineContext, _event: OID4VCIMachineEventTypes): boolean => { + const { verificationCode } = _ctx + return verificationCode !== undefined && verificationCode.length > 0 +} + +const oid4vciCreateContactGuard = (_ctx: OID4VCIMachineContext, _event: OID4VCIMachineEventTypes): boolean => { + const { contactAlias, hasContactConsent } = _ctx + return hasContactConsent && contactAlias !== undefined && contactAlias.length > 0 +} + +const oid4vciHasSelectedCredentialsGuard = (_ctx: OID4VCIMachineContext, _event: OID4VCIMachineEventTypes): boolean => { + const { selectedCredentials } = _ctx + return selectedCredentials !== undefined && selectedCredentials.length > 0 +} + +// FIXME refactor this guard +const oid4vciRequireAuthorizationGuard = (_ctx: OID4VCIMachineContext, _event: OID4VCIMachineEventTypes): boolean => { + const { openID4VCIClientState } = _ctx + + if (!openID4VCIClientState) { + throw Error('Missing openID4VCI client state in context') + } + + if (!openID4VCIClientState.credentialOffer?.supportedFlows ?? (openID4VCIClientState.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint + ? [AuthzFlowType.AUTHORIZATION_CODE_FLOW] + : [] + )) { + return false; + } else if (!openID4VCIClientState.authorizationURL) { + return false; + } + + return !openID4VCIClientState.accessTokenResponse +} + +const createOID4VCIMachine = (opts?: CreateOID4VCIMachineOpts): OID4VCIStateMachine => { + const initialContext: OID4VCIMachineContext = { + // TODO WAL-671 we need to store the data from OpenIdProvider here in the context and make sure we can restart the machine with it and init the OpenIdProvider + requestData: opts?.requestData, + locale: opts?.locale, + credentialsSupported: [], + credentialSelection: [], + selectedCredentials: [], + credentialsToAccept: [], + hasContactConsent: true, + contactAlias: '', + } + + return createMachine({ + id: opts?.machineId ?? uuidv4(), + predictableActionArguments: true, + initial: OID4VCIMachineStates.initiateOID4VCI, + schema: { + events: {} as OID4VCIMachineEventTypes, + guards: {} as + | { type: OID4VCIMachineGuards.hasNoContactGuard } + | { type: OID4VCIMachineGuards.selectCredentialGuard } + | { type: OID4VCIMachineGuards.requirePinGuard } + | { type: OID4VCIMachineGuards.requireAuthorizationGuard } + | { type: OID4VCIMachineGuards.hasNoContactIdentityGuard } + | { type: OID4VCIMachineGuards.verificationCodeGuard } + | { type: OID4VCIMachineGuards.hasContactGuard } + | { type: OID4VCIMachineGuards.createContactGuard } + | { type: OID4VCIMachineGuards.hasSelectedCredentialsGuard }, + services: {} as { + [OID4VCIMachineServices.initiateOID4VCI]: { + data: InitiationData + } + [OID4VCIMachineServices.createCredentialSelection]: { + data: Array + } + [OID4VCIMachineServices.getContact]: { + data: Party | undefined + } + [OID4VCIMachineServices.getCredentials]: { + data: Array | undefined + } + [OID4VCIMachineServices.addContactIdentity]: { + data: void + } + [OID4VCIMachineServices.assertValidCredentials]: { + data: void + } + [OID4VCIMachineServices.storeCredentialBranding]: { + data: void + } + [OID4VCIMachineServices.storeCredentials]: { + data: void + } + }, + }, + context: initialContext, + states: { + [OID4VCIMachineStates.initiateOID4VCI]: { + id: OID4VCIMachineStates.initiateOID4VCI, + invoke: { + src: OID4VCIMachineServices.initiateOID4VCI, + onDone: { + target: OID4VCIMachineStates.createCredentialSelection, + actions: assign({ + authorizationCodeURL: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent) => _event.data.authorizationCodeURL, + credentialBranding: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent) => _event.data.credentialBranding, + credentialsSupported: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent) => _event.data.credentialsSupported, + serverMetadata: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent) => _event.data.serverMetadata, + openID4VCIClientState: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent) => _event.data.openID4VCIClientState, + }), + }, + onError: { + target: OID4VCIMachineStates.handleError, + actions: assign({ + error: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent): ErrorDetails => ({ + title: translate('oid4vci_machine_initiation_error_title'), + message: _event.data.message, + }), + }), + }, + }, + }, + [OID4VCIMachineStates.createCredentialSelection]: { + id: OID4VCIMachineStates.createCredentialSelection, + invoke: { + src: OID4VCIMachineServices.createCredentialSelection, + onDone: { + target: OID4VCIMachineStates.getContact, + actions: assign({ + credentialSelection: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent>) => _event.data, + }), + // TODO WAL-670 would be nice if we can have guard that checks if we have at least 1 item in the selection. not sure if this can occur but it would be more defensive. + // Still cannot find a nice way to do this inside of an invoke besides adding another transition state + }, + onError: { + target: OID4VCIMachineStates.handleError, + actions: assign({ + error: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent): ErrorDetails => ({ + title: translate('oid4vci_machine_credential_selection_error_title'), + message: _event.data.message, + }), + }), + }, + }, + }, + [OID4VCIMachineStates.getContact]: { + id: OID4VCIMachineStates.getContact, + invoke: { + src: OID4VCIMachineServices.getContact, + onDone: { + target: OID4VCIMachineStates.transitionFromSetup, + actions: assign({ contact: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent) => _event.data }), + }, + onError: { + target: OID4VCIMachineStates.handleError, + actions: assign({ + error: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent): ErrorDetails => ({ + title: translate('oid4vci_machine_retrieve_contact_error_title'), + message: _event.data.message, + }), + }), + }, + }, + }, + [OID4VCIMachineStates.transitionFromSetup]: { + id: OID4VCIMachineStates.transitionFromSetup, + always: [ + { + target: OID4VCIMachineStates.addContact, + cond: OID4VCIMachineGuards.hasNoContactGuard, + }, + { + target: OID4VCIMachineStates.selectCredentials, + cond: OID4VCIMachineGuards.selectCredentialGuard, + }, + { + target: OID4VCIMachineStates.verifyPin, + cond: OID4VCIMachineGuards.requirePinGuard, + }, + { + target: OID4VCIMachineStates.initiateAuthorizationRequest, + cond: OID4VCIMachineGuards.requireAuthorizationGuard, + }, + { + target: OID4VCIMachineStates.getCredentials, + }, + ], + on: { + [OID4VCIMachineEvents.SET_AUTHORIZATION_CODE_URL]: { + actions: assign({ authorizationCodeURL: (_ctx: OID4VCIMachineContext, _event: SetAuthorizationCodeURLEvent) => _event.data }), + }, + }, + }, + [OID4VCIMachineStates.addContact]: { + id: OID4VCIMachineStates.addContact, + initial: OID4VCIMachineAddContactStates.idle, + on: { + [OID4VCIMachineEvents.SET_CONTACT_CONSENT]: { + actions: assign({ hasContactConsent: (_ctx: OID4VCIMachineContext, _event: ContactConsentEvent) => _event.data }), + }, + [OID4VCIMachineEvents.SET_CONTACT_ALIAS]: { + actions: assign({ contactAlias: (_ctx: OID4VCIMachineContext, _event: ContactAliasEvent) => _event.data }), + }, + [OID4VCIMachineEvents.CREATE_CONTACT]: { + target: `.${OID4VCIMachineAddContactStates.next}`, + actions: assign({ contact: (_ctx: OID4VCIMachineContext, _event: CreateContactEvent) => _event.data }), + cond: OID4VCIMachineGuards.createContactGuard, + }, + [OID4VCIMachineEvents.DECLINE]: { + target: OID4VCIMachineStates.declined, + }, + [OID4VCIMachineEvents.PREVIOUS]: { + target: OID4VCIMachineStates.aborted, + }, + }, + states: { + [OID4VCIMachineAddContactStates.idle]: {}, + [OID4VCIMachineAddContactStates.next]: { + always: { + target: `#${OID4VCIMachineStates.transitionFromContactSetup}`, + cond: OID4VCIMachineGuards.hasContactGuard, + }, + }, + }, + }, + [OID4VCIMachineStates.transitionFromContactSetup]: { + id: OID4VCIMachineStates.transitionFromContactSetup, + always: [ + { + target: OID4VCIMachineStates.selectCredentials, + cond: OID4VCIMachineGuards.selectCredentialGuard, + }, + { + target: OID4VCIMachineStates.verifyPin, + cond: OID4VCIMachineGuards.requirePinGuard, + }, + // TODO are we not missing initiateAuthorizationRequest here??? + { + target: OID4VCIMachineStates.getCredentials, + }, + ], + }, + [OID4VCIMachineStates.selectCredentials]: { + id: OID4VCIMachineStates.selectCredentials, + on: { + [OID4VCIMachineEvents.SET_SELECTED_CREDENTIALS]: { + actions: assign({ selectedCredentials: (_ctx: OID4VCIMachineContext, _event: SelectCredentialsEvent) => _event.data }), + }, + [OID4VCIMachineEvents.NEXT]: { + target: OID4VCIMachineStates.transitionFromSelectingCredentials, + cond: OID4VCIMachineGuards.hasSelectedCredentialsGuard, + }, + [OID4VCIMachineEvents.PREVIOUS]: { + target: OID4VCIMachineStates.aborted, + }, + }, + }, + [OID4VCIMachineStates.transitionFromSelectingCredentials]: { + id: OID4VCIMachineStates.transitionFromSelectingCredentials, + always: [ + { + target: OID4VCIMachineStates.verifyPin, + cond: OID4VCIMachineGuards.requirePinGuard, + }, + // TODO missing initiateAuthorizationRequest ?? + { + target: OID4VCIMachineStates.getCredentials, + }, + ], + }, + [OID4VCIMachineStates.initiateAuthorizationRequest]: { + id: OID4VCIMachineStates.initiateAuthorizationRequest, + on: { + [OID4VCIMachineEvents.PREVIOUS]: { + target: OID4VCIMachineStates.selectCredentials, + }, + [OID4VCIMachineEvents.INVOKED_AUTHORIZATION_CODE_REQUEST]: { + target: OID4VCIMachineStates.waitForAuthorizationResponse, + }, + }, + }, + [OID4VCIMachineStates.waitForAuthorizationResponse]: { + id: OID4VCIMachineStates.waitForAuthorizationResponse, + on: { + [OID4VCIMachineEvents.PREVIOUS]: { + target: OID4VCIMachineStates.initiateAuthorizationRequest, + }, + [OID4VCIMachineEvents.PROVIDE_AUTHORIZATION_CODE_RESPONSE]: { + target: OID4VCIMachineStates.transitionFromSelectingCredentials, + actions: assign({ + authorizationCodeResponse: (_ctx: OID4VCIMachineContext, _event: AuthorizationResponseEvent) => + toAuthorizationResponsePayload(_event.data), + }), // TODO can we not call toAuthorizationResponsePayload before + }, + }, + }, + [OID4VCIMachineStates.verifyPin]: { + id: OID4VCIMachineStates.verifyPin, + initial: OID4VCIMachineVerifyPinStates.idle, + on: { + [OID4VCIMachineEvents.SET_VERIFICATION_CODE]: { + target: `.${OID4VCIMachineVerifyPinStates.next}`, + actions: assign({ verificationCode: (_ctx: OID4VCIMachineContext, _event: VerificationCodeEvent) => _event.data }), + }, + [OID4VCIMachineEvents.PREVIOUS]: [ + { + target: OID4VCIMachineStates.selectCredentials, + cond: OID4VCIMachineGuards.selectCredentialGuard, + }, + { + target: OID4VCIMachineStates.aborted, + }, + ], + }, + states: { + [OID4VCIMachineVerifyPinStates.idle]: {}, + [OID4VCIMachineVerifyPinStates.next]: { + always: { + target: `#${OID4VCIMachineStates.getCredentials}`, + cond: OID4VCIMachineGuards.verificationCodeGuard, + }, + }, + }, + }, + [OID4VCIMachineStates.getCredentials]: { + id: OID4VCIMachineStates.getCredentials, + invoke: { + src: OID4VCIMachineServices.getCredentials, + onDone: { + target: OID4VCIMachineStates.verifyCredentials, + actions: assign({ + credentialsToAccept: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent>) => _event.data, + }), + }, + onError: { + target: OID4VCIMachineStates.handleError, + actions: assign({ + error: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent): ErrorDetails => ({ + title: translate('oid4vci_machine_retrieve_credentials_error_title'), + message: _event.data.message, + }), + }), + }, + }, + exit: assign({ verificationCode: undefined }), + }, + [OID4VCIMachineStates.verifyCredentials]: { + id: OID4VCIMachineStates.verifyCredentials, + invoke: { + src: OID4VCIMachineServices.assertValidCredentials, + onDone: { + target: OID4VCIMachineStates.transitionFromWalletInput, + }, + onError: { + target: OID4VCIMachineStates.handleError, + actions: assign({ + error: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent): ErrorDetails => ({ + title: translate('oid4vci_machine_verify_credentials_error_title'), + message: _event.data.message, + }), + }), + }, + }, + }, + [OID4VCIMachineStates.transitionFromWalletInput]: { + id: OID4VCIMachineStates.transitionFromWalletInput, + always: [ + { + target: OID4VCIMachineStates.addContactIdentity, + cond: OID4VCIMachineGuards.hasNoContactIdentityGuard, + }, + { + target: OID4VCIMachineStates.reviewCredentials, + }, + ], + }, + [OID4VCIMachineStates.addContactIdentity]: { + id: OID4VCIMachineStates.addContactIdentity, + invoke: { + src: OID4VCIMachineServices.addContactIdentity, + onDone: { + target: OID4VCIMachineStates.reviewCredentials, + actions: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent): void => { + _ctx.contact?.identities.push(_event.data) + }, + }, + onError: { + target: OID4VCIMachineStates.handleError, + actions: assign({ + error: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent): ErrorDetails => ({ + title: translate('oid4vci_machine_add_contact_identity_error_title'), + message: _event.data.message, + }), + }), + }, + }, + }, + [OID4VCIMachineStates.reviewCredentials]: { + id: OID4VCIMachineStates.reviewCredentials, + on: { + [OID4VCIMachineEvents.NEXT]: { + target: OID4VCIMachineStates.storeCredentialBranding, + }, + [OID4VCIMachineEvents.DECLINE]: { + target: OID4VCIMachineStates.declined, + }, + [OID4VCIMachineEvents.PREVIOUS]: { + target: OID4VCIMachineStates.aborted, + }, + }, + }, + [OID4VCIMachineStates.storeCredentialBranding]: { + id: OID4VCIMachineStates.storeCredentialBranding, + invoke: { + src: OID4VCIMachineServices.storeCredentialBranding, + onDone: { + target: OID4VCIMachineStates.storeCredentials, + }, + onError: { + target: OID4VCIMachineStates.handleError, + actions: assign({ + error: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent): ErrorDetails => ({ + title: translate('oid4vci_machine_store_credential_branding_error_title'), + message: _event.data.message, + }), + }), + }, + }, + }, + [OID4VCIMachineStates.storeCredentials]: { + id: OID4VCIMachineStates.storeCredentials, + invoke: { + src: OID4VCIMachineServices.storeCredentials, + onDone: { + target: OID4VCIMachineStates.done, + }, + onError: { + target: OID4VCIMachineStates.handleError, + actions: assign({ + error: (_ctx: OID4VCIMachineContext, _event: DoneInvokeEvent): ErrorDetails => ({ + title: translate('oid4vci_machine_store_credential_error_title'), + message: _event.data.message, + }), + }), + }, + }, + }, + [OID4VCIMachineStates.handleError]: { + id: OID4VCIMachineStates.handleError, + on: { + [OID4VCIMachineEvents.NEXT]: { + target: OID4VCIMachineStates.error, + }, + [OID4VCIMachineEvents.PREVIOUS]: { + target: OID4VCIMachineStates.error, + }, + }, + }, + [OID4VCIMachineStates.aborted]: { + id: OID4VCIMachineStates.aborted, + type: 'final', + }, + [OID4VCIMachineStates.declined]: { + id: OID4VCIMachineStates.declined, + type: 'final', + }, + [OID4VCIMachineStates.error]: { + id: OID4VCIMachineStates.error, + type: 'final', + }, + [OID4VCIMachineStates.done]: { + id: OID4VCIMachineStates.done, + type: 'final', + }, + }, + }) +} + +export class OID4VCIMachine { + static newInstance(opts?: OID4VCIMachineInstanceOpts): OID4VCIMachineInterpreter { + const instance: OID4VCIMachineInterpreter = interpret( + createOID4VCIMachine(opts).withConfig({ + services: { + ...opts?.services, + }, + guards: { + oid4vciHasNoContactGuard, + oid4vciSelectCredentialsGuard, + oid4vciRequirePinGuard, + oid4vciHasNoContactIdentityGuard, + oid4vciVerificationCodeGuard, + oid4vciHasContactGuard, + oid4vciCreateContactGuard, + oid4vciHasSelectedCredentialsGuard, + oid4vciRequireAuthorizationGuard, + ...opts?.guards, + }, + }) + ) + + if (typeof opts?.subscription === 'function') { + instance.onTransition(opts.subscription) + } else if (opts?.requireCustomNavigationHook !== true) { + instance.onTransition((snapshot: OID4VCIMachineState): void => { + opts?.stateNavigationListener(instance, snapshot) + }) + } + + return instance + } +} diff --git a/packages/oid4vci-holder/src/types/IOID4VCIHolder.ts b/packages/oid4vci-holder/src/types/IOID4VCIHolder.ts new file mode 100644 index 000000000..85187890e --- /dev/null +++ b/packages/oid4vci-holder/src/types/IOID4VCIHolder.ts @@ -0,0 +1,346 @@ +import { OpenID4VCIClient, OpenID4VCIClientState } from '@sphereon/oid4vci-client' +import { + AuthorizationResponse, + CredentialResponse, + CredentialSupported, + EndpointMetadataResult +} from '@sphereon/oid4vci-common' +import { IContactManager } from '@sphereon/ssi-sdk.contact-manager' +import { IBasicCredentialLocaleBranding, IBasicIssuerLocaleBranding, Identity, Party } from '@sphereon/ssi-sdk.data-store' +import { IIssuanceBranding } from '@sphereon/ssi-sdk.issuance-branding' +import { IVerifiableCredential, WrappedVerifiableCredential, WrappedVerifiablePresentation } from '@sphereon/ssi-types' +import { IAgentContext, ICredentialPlugin, IPluginMethodMap, TKeyType, VerifiableCredential } from '@veramo/core' +import { IDataStore, IDataStoreORM } from '@veramo/data-store' +import { BaseActionObject, Interpreter, ResolveTypegenMeta, ServiceMap, State, StateMachine, TypegenDisabled } from 'xstate' + +export interface IOID4VCIHolder extends IPluginMethodMap { + oid4vciHolderGetMachineInterpreter(args: GetMachineArgs, context: RequiredContext): Promise + oid4vciHolderGetInitiationData(args: InitiateOID4VCIArgs, context: RequiredContext): Promise + oid4vciHolderCreateCredentialSelection(args: CreateCredentialSelectionArgs, context: RequiredContext): Promise> + oid4vciHolderGetContact(args: GetContactArgs, context: RequiredContext): Promise + oid4vciHolderGetCredentials(args: GetCredentialsArgs, context: RequiredContext): Promise | undefined> + oid4vciHolderAddContactIdentity(args: AddContactIdentityArgs, context: RequiredContext): Promise + oid4vciHolderAssertValidCredentials(args: AssertValidCredentialsArgs, context: RequiredContext): Promise + oid4vciHolderStoreCredentialBranding(args: StoreCredentialBrandingArgs, context: RequiredContext): Promise + oid4vciHolderStoreCredentials(args: StoreCredentialsArgs, context: RequiredContext): Promise +} + +export type OID4VCIHolderOptions = { + onContactIdentityCreated?: (args: OnContactIdentityCreatedArgs) => Promise + onCredentialStored?: (args: OnCredentialStoredArgs) => Promise + onGetCredentials: (args: OnCredentialsArgs) => Promise> + vcFormatPreferences?: Array +} + +export type OnContactIdentityCreatedArgs = { + contactId: string + identity: Identity +} + +export type OnCredentialStoredArgs = { + vcHash: string + credential: VerifiableCredential +} + +export type GetMachineArgs = { + requestData: RequestData + stateNavigationListener?: (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState, navigation?: any) => Promise +} + +export type InitiateOID4VCIArgs = Pick +export type CreateCredentialSelectionArgs = Pick +export type GetContactArgs = Pick +export type GetCredentialsArgs = Pick +export type AddContactIdentityArgs = Pick +export type AssertValidCredentialsArgs = Pick +export type StoreCredentialBrandingArgs = Pick +export type StoreCredentialsArgs = Pick + +export enum OID4VCIHolderEvent { + CONTACT_IDENTITY_CREATED = 'contact_identity_created', + CREDENTIAL_STORED = 'credential_stored', +} + +export type RequestData = { + credentialOffer: any + uri: string + [x: string]: any +} + +export enum SupportedLanguage { + ENGLISH = 'en', + DUTCH = 'nl', +} + +export type VerifyCredentialToAcceptArgs = { + mappedCredential: MappedCredentialToAccept + context: RequiredContext +} + +export type MappedCredentialToAccept = { + correlationId: string + credential: CredentialToAccept; + uniformVerifiableCredential: IVerifiableCredential + rawVerifiableCredential: VerifiableCredential +} + +export type OID4VCIMachineContext = { + requestData?: RequestData // TODO WAL-673 fix type as this is not always a qr code (deeplink) + locale?: string + authorizationCodeURL?: string + credentialBranding?: Map> + credentialsSupported: Array + serverMetadata?: EndpointMetadataResult + openID4VCIClientState?: OpenID4VCIClientState + credentialSelection: Array + contactAlias: string + contact?: Party + selectedCredentials: Array + authorizationCodeResponse?: AuthorizationResponse + credentialsToAccept: Array + verificationCode?: string // TODO WAL-672 refactor to not store verificationCode in the context + hasContactConsent: boolean + error?: ErrorDetails +} + +export enum OID4VCIMachineStates { + initiateOID4VCI = 'initiateOID4VCI', + createCredentialSelection = 'createCredentialSelection', + getContact = 'getContact', + transitionFromSetup = 'transitionFromSetup', + addContact = 'addContact', + transitionFromContactSetup = 'transitionFromContactSetup', + selectCredentials = 'selectCredentials', + transitionFromSelectingCredentials = 'transitionFromSelectingCredentials', + verifyPin = 'verifyPin', + initiateAuthorizationRequest = 'initiateAuthorizationRequest', + waitForAuthorizationResponse = 'waitForAuthorizationResponse', + getCredentials = 'getCredentials', + transitionFromWalletInput = 'transitionFromWalletInput', + addContactIdentity = 'addContactIdentity', + reviewCredentials = 'reviewCredentials', + verifyCredentials = 'verifyCredentials', + storeCredentialBranding = 'storeCredentialBranding', + storeCredentials = 'storeCredentials', + handleError = 'handleError', + aborted = 'aborted', + declined = 'declined', + error = 'error', + done = 'done', +} + +export enum OID4VCIMachineAddContactStates { + idle = 'idle', + next = 'next', +} + +export enum OID4VCIMachineVerifyPinStates { + idle = 'idle', + next = 'next', +} + +export type OID4VCIMachineInterpreter = Interpreter< + OID4VCIMachineContext, + any, + OID4VCIMachineEventTypes, + { value: any; context: OID4VCIMachineContext }, + any +> + +export type OID4VCIMachineState = State + +export type OID4VCIStateMachine = StateMachine< + OID4VCIMachineContext, + any, + OID4VCIMachineEventTypes, + { value: any; context: OID4VCIMachineContext }, + BaseActionObject, + ServiceMap, + ResolveTypegenMeta +> + +export type CreateOID4VCIMachineOpts = { + requestData: RequestData + machineId?: string + locale?: string +} + +export type OID4VCIMachineInstanceOpts = { + services?: any + guards?: any + subscription?: () => void + requireCustomNavigationHook?: boolean + stateNavigationListener: (oid4vciMachine: OID4VCIMachineInterpreter, state: OID4VCIMachineState, navigation?: any) => Promise +} & CreateOID4VCIMachineOpts + +export type OID4VCIProviderProps = { + children?: any + customOID4VCIInstance?: OID4VCIMachineInterpreter +} + +export type OID4VCIContext = { + oid4vciInstance?: OID4VCIMachineInterpreter +} + +export type OID4VCIMachineNavigationArgs = { + oid4vciMachine: OID4VCIMachineInterpreter + state: OID4VCIMachineState + navigation: any + onNext?: () => void + onBack?: () => void +} + +export enum OID4VCIMachineEvents { + NEXT = 'NEXT', + PREVIOUS = 'PREVIOUS', + DECLINE = 'DECLINE', + CREATE_CONTACT = 'CREATE_CONTACT', + SET_VERIFICATION_CODE = 'SET_VERIFICATION_CODE', + SET_CONTACT_ALIAS = 'SET_CONTACT_ALIAS', + SET_CONTACT_CONSENT = 'SET_CONTACT_CONSENT', + SET_SELECTED_CREDENTIALS = 'SET_SELECTED_CREDENTIALS', + SET_AUTHORIZATION_CODE_URL = 'SET_AUTHORIZATION_CODE_URL', + INVOKED_AUTHORIZATION_CODE_REQUEST = 'INVOKED_AUTHORIZATION_CODE_REQUEST', + PROVIDE_AUTHORIZATION_CODE_RESPONSE = 'PROVIDE_AUTHORIZATION_CODE_RESPONSE', +} + +export enum OID4VCIMachineGuards { + hasContactGuard = 'oid4vciHasContactGuard', + hasNoContactGuard = 'oid4vciHasNoContactGuard', + selectCredentialGuard = 'oid4vciSelectCredentialsGuard', + requirePinGuard = 'oid4vciRequirePinGuard', + requireAuthorizationGuard = 'oid4vciRequireAuthorizationGuard', + hasNoContactIdentityGuard = 'oid4vciHasNoContactIdentityGuard', + verificationCodeGuard = 'oid4vciVerificationCodeGuard', + createContactGuard = 'oid4vciCreateContactGuard', + hasSelectedCredentialsGuard = 'oid4vciHasSelectedCredentialsGuard', +} + +export enum OID4VCIMachineServices { + initiateOID4VCI = 'initiateOID4VCI', + getContact = 'getContact', + addContactIdentity = 'addContactIdentity', + createCredentialSelection = 'createCredentialSelection', + getCredentials = 'getCredentials', + assertValidCredentials = 'assertValidCredentials', + storeCredentialBranding = 'storeCredentialBranding', + storeCredentials = 'storeCredentials', +} + +export type NextEvent = { type: OID4VCIMachineEvents.NEXT } +export type PreviousEvent = { type: OID4VCIMachineEvents.PREVIOUS } +export type DeclineEvent = { type: OID4VCIMachineEvents.DECLINE } +export type CreateContactEvent = { type: OID4VCIMachineEvents.CREATE_CONTACT; data: Party } +export type SelectCredentialsEvent = { type: OID4VCIMachineEvents.SET_SELECTED_CREDENTIALS; data: Array } +export type VerificationCodeEvent = { type: OID4VCIMachineEvents.SET_VERIFICATION_CODE; data: string } +export type ContactConsentEvent = { type: OID4VCIMachineEvents.SET_CONTACT_CONSENT; data: boolean } +export type ContactAliasEvent = { type: OID4VCIMachineEvents.SET_CONTACT_ALIAS; data: string } +export type SetAuthorizationCodeURLEvent = { type: OID4VCIMachineEvents.SET_AUTHORIZATION_CODE_URL; data: string } +export type InvokeAuthorizationRequestEvent = { type: OID4VCIMachineEvents.INVOKED_AUTHORIZATION_CODE_REQUEST; data: string } +export type AuthorizationResponseEvent = { type: OID4VCIMachineEvents.PROVIDE_AUTHORIZATION_CODE_RESPONSE; data: string | AuthorizationResponse } +export type OID4VCIMachineEventTypes = + | NextEvent + | PreviousEvent + | DeclineEvent + | CreateContactEvent + | SelectCredentialsEvent + | VerificationCodeEvent + | ContactConsentEvent + | ContactAliasEvent + | SetAuthorizationCodeURLEvent + | InvokeAuthorizationRequestEvent + | AuthorizationResponseEvent + +export type ErrorDetails = { + title: string + message: string + // TODO WAL-676 would be nice if we can bundle these details fields into a new type so that we can check on this field instead of the 2 separately + detailsTitle?: string + detailsMessage?: string +} + +export enum RequestType { + OPENID_INITIATE_ISSUANCE = 'openid-initiate-issuance', + OPENID_CREDENTIAL_OFFER = 'openid-credential-offer', +} + +export type CredentialTypeSelection = { + id: string + credentialType: string + credentialAlias: string + isSelected: boolean +} + +export type InitiationData = { + authorizationCodeURL?: string + credentialBranding: Map> + credentialsSupported: Array + serverMetadata: EndpointMetadataResult + openID4VCIClientState: OpenID4VCIClientState +} + +export type SelectAppLocaleBrandingArgs = { + locale?: string + localeBranding?: Array +} + +export interface OnCredentialsArgs { + pin?: string + credentials?: Array + openID4VCIClientState: OpenID4VCIClientState +} + +export type IssuanceOpts = CredentialSupported & { + didMethod: SupportedDidMethodEnum + keyType: TKeyType +} + +export enum SupportedDidMethodEnum { + DID_ETHR = 'ethr', + DID_KEY = 'key', + DID_LTO = 'lto', + DID_ION = 'ion', + DID_FACTOM = 'factom', + DID_JWK = 'jwk', +} + +export type VerificationResult = { + result: boolean + source: WrappedVerifiableCredential | WrappedVerifiablePresentation + subResults: Array + error?: string | undefined + errorDetails?: string +} + +export type VerificationSubResult = { + result: boolean + error?: string + errorDetails?: string +} + +export type CredentialToAccept = { + id?: string + issuanceOpt: IssuanceOpts + credentialResponse: CredentialResponse +} + +export type GetSupportedCredentialsArgs = { + openID4VCIClient: OpenID4VCIClient, + vcFormatPreferences: Array +} + +export type GetCredentialBrandingArgs = { + credentialsSupported: Array + context: RequiredContext +} + +export type GetPreferredCredentialFormatsArgs = { + credentials: Array + vcFormatPreferences: Array +} + +export type MapCredentialToAcceptArgs = { + credentials: Array +} + +export type RequiredContext = IAgentContext diff --git a/packages/oid4vci-holder/tsconfig.json b/packages/oid4vci-holder/tsconfig.json new file mode 100644 index 000000000..6066b2338 --- /dev/null +++ b/packages/oid4vci-holder/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declarationDir": "dist", + "strictPropertyInitialization": false + }, + "include": ["src/**/*.json", "src/**/*.ts"], + "references": [ + { + "path": "../agent-config" + }, + { + "path": "../contact-manager" + }, + { + "path": "../data-store" + }, + { + "path": "../issuance-branding" + }, + { + "path": "../ssi-types" + } + ] +} diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 7c0eda635..2c9a054ae 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -17,6 +17,7 @@ { "path": "vc-status-list-issuer-rest-api" }, { "path": "vc-handler-ld-local" }, { "path": "presentation-exchange" }, + { "path": "oid4vci-holder" }, { "path": "oid4vci-issuer-store" }, { "path": "oid4vci-issuer" }, { "path": "oid4vci-issuer-rest-api" },