diff --git a/packages/cli/package.json b/packages/cli/package.json index 715e47e2..c6ea5efa 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -46,16 +46,17 @@ "@vckit/remote-client": "^1.0.0-beta.5", "@vckit/remote-server": "^1.0.0-beta.5", "@vckit/renderer": "^1.0.0-beta.5", - "@veramo/credential-eip712": "^5.1.2", - "@veramo/credential-ld": "^5.1.2", - "@veramo/credential-w3c": "^5.1.2", - "@veramo/data-store": "^5.1.2", - "@veramo/did-comm": "^5.1.2", + "@vckit/vc-api": "workspace:1.0.0-beta.5", + "@veramo/credential-eip712": "^5.2.0", + "@veramo/credential-ld": "^5.2.0", + "@veramo/credential-w3c": "^5.2.0", + "@veramo/data-store": "^5.2.0", + "@veramo/did-comm": "^5.2.0", "@veramo/did-discovery": "^5.1.2", - "@veramo/did-jwt": "^5.1.2", + "@veramo/did-jwt": "^5.2.0", "@veramo/did-manager": "^5.1.2", "@veramo/did-provider-ethr": "^5.1.2", - "@veramo/did-provider-key": "^5.1.2", + "@veramo/did-provider-key": "^5.2.0", "@veramo/did-provider-pkh": "^5.1.2", "@veramo/did-provider-web": "^5.1.2", "@veramo/did-resolver": "^5.1.2", @@ -64,7 +65,7 @@ "@veramo/message-handler": "^5.1.2", "@veramo/selective-disclosure": "^5.1.2", "@veramo/url-handler": "^5.1.2", - "@veramo/utils": "^5.1.2", + "@veramo/utils": "^5.2.0", "blessed": "^0.1.81", "commander": "^10.0.0", "console-table-printer": "^2.10.0", diff --git a/packages/credential-ld/package.json b/packages/credential-ld/package.json index a8091688..ce176ff5 100644 --- a/packages/credential-ld/package.json +++ b/packages/credential-ld/package.json @@ -28,7 +28,7 @@ "@transmute/json-web-signature": "^0.7.0-unstable.79", "@vckit/core-types": "^1.0.0-beta.5", "@veramo-community/lds-ecdsa-secp256k1-recovery2020": "uport-project/EcdsaSecp256k1RecoverySignature2020", - "@veramo/utils": "^5.1.2", + "@veramo/utils": "^5.2.0", "cross-fetch": "^3.1.5", "debug": "^4.3.3", "did-resolver": "^4.0.1", diff --git a/packages/credential-oa/package.json b/packages/credential-oa/package.json index 3a747a94..b8167c2c 100644 --- a/packages/credential-oa/package.json +++ b/packages/credential-oa/package.json @@ -18,7 +18,7 @@ "@govtechsg/open-attestation": "^6.6.0", "@vckit/core-types": "^1.0.0-beta.5", "@veramo/message-handler": "^5.1.2", - "@veramo/utils": "^5.1.2" + "@veramo/utils": "^5.2.0" }, "devDependencies": { "@types/debug": "4.1.7", diff --git a/packages/credential-w3c/package.json b/packages/credential-w3c/package.json index d234f7ec..a1b7feef 100644 --- a/packages/credential-w3c/package.json +++ b/packages/credential-w3c/package.json @@ -17,7 +17,7 @@ "@govtechsg/open-attestation": "^6.6.0", "@vckit/core-types": "^1.0.0-beta.5", "@veramo/message-handler": "^5.1.2", - "@veramo/utils": "^5.1.2", + "@veramo/utils": "^5.2.0", "canonicalize": "^1.0.8", "debug": "^4.3.3", "did-jwt": "^6.11.0", diff --git a/packages/remote-server/package.json b/packages/remote-server/package.json index e07380b7..2a71ad75 100644 --- a/packages/remote-server/package.json +++ b/packages/remote-server/package.json @@ -14,12 +14,14 @@ "@vckit/app": "^1.0.0-beta.4", "@vckit/core-types": "^1.0.0-beta.5", "@vckit/remote-client": "^1.0.0-beta.5", + "@veramo/utils": "5.2.0", "debug": "^4.3.3", "did-resolver": "^4.0.1", "express": "^4.18.2", "passport": "^0.6.0", "passport-http-bearer": "^1.0.1", - "url-parse": "^1.5.4" + "url-parse": "^1.5.4", + "uint8arrays": "^4.0.3" }, "devDependencies": { "@types/debug": "4.1.7", diff --git a/packages/remote-server/src/web-did-doc-router.ts b/packages/remote-server/src/web-did-doc-router.ts index b5b643a4..867efcce 100644 --- a/packages/remote-server/src/web-did-doc-router.ts +++ b/packages/remote-server/src/web-did-doc-router.ts @@ -1,9 +1,10 @@ -import { IIdentifier, IDIDManager, TAgent, TKeyType } from '@vckit/core-types' -import { Request, Router } from 'express' -import { ServiceEndpoint } from 'did-resolver' +import { IIdentifier, IDIDManager, TAgent, TKeyType } from '@vckit/core-types'; +import { Request, Router } from 'express'; +import { ServiceEndpoint } from 'did-resolver'; +import * as u8a from 'uint8arrays'; interface RequestWithAgentDIDManager extends Request { - agent?: TAgent + agent?: TAgent; } /** @@ -11,22 +12,22 @@ interface RequestWithAgentDIDManager extends Request { * * @public */ -export const didDocEndpoint = '/.well-known/did.json' +export const didDocEndpoint = '/.well-known/did.json'; const keyMapping: Record = { Secp256k1: 'EcdsaSecp256k1VerificationKey2019', Secp256r1: 'EcdsaSecp256r1VerificationKey2019', - Ed25519: 'Ed25519VerificationKey2018', - X25519: 'X25519KeyAgreementKey2019', + Ed25519: 'Ed25519VerificationKey2020', + X25519: 'X25519KeyAgreementKey2020', Bls12381G1: 'Bls12381G1Key2020', Bls12381G2: 'Bls12381G2Key2020', -} +}; /** * @public */ export interface WebDidDocRouterOptions { - services?: ServiceEndpoint[] + services?: ServiceEndpoint[]; } /** @@ -38,22 +39,26 @@ export interface WebDidDocRouterOptions { * @public */ export const WebDidDocRouter = (options: WebDidDocRouterOptions): Router => { - const router = Router() + const router = Router(); const didDocForIdentifier = (identifier: IIdentifier) => { const allKeys = identifier.keys.map((key) => ({ id: identifier.did + '#' + key.kid, type: keyMapping[key.type], controller: identifier.did, - publicKeyHex: key.publicKeyHex, - })) + publicKeyMultibase: hexToMultibase(key.publicKeyHex), + })); // ed25519 keys can also be converted to x25519 for key agreement const keyAgreementKeyIds = allKeys - .filter((key) => ['Ed25519VerificationKey2018', 'X25519KeyAgreementKey2019'].includes(key.type)) - .map((key) => key.id) + .filter((key) => + ['Ed25519VerificationKey2020', 'X25519KeyAgreementKey2020'].includes( + key.type + ) + ) + .map((key) => key.id); const signingKeyIds = allKeys - .filter((key) => key.type !== 'X25519KeyAgreementKey2019') - .map((key) => key.id) + .filter((key) => key.type !== 'X25519KeyAgreementKey2020') + .map((key) => key.id); const didDoc = { '@context': 'https://w3id.org/did/v1', @@ -63,42 +68,66 @@ export const WebDidDocRouter = (options: WebDidDocRouterOptions): Router => { assertionMethod: signingKeyIds, keyAgreement: keyAgreementKeyIds, service: [...(options?.services || []), ...(identifier?.services || [])], - } + }; - return didDoc - } + return didDoc; + }; const getAliasForRequest = (req: Request) => { - return encodeURIComponent(req.get('host') || req.hostname) - } + return encodeURIComponent(req.get('host') || req.hostname); + }; router.get(didDocEndpoint, async (req: RequestWithAgentDIDManager, res) => { if (req.agent) { try { const serverIdentifier = await req.agent.didManagerGet({ did: 'did:web:' + getAliasForRequest(req), - }) - const didDoc = didDocForIdentifier(serverIdentifier) - res.json(didDoc) + }); + const didDoc = didDocForIdentifier(serverIdentifier); + res.json(didDoc); } catch (e) { - res.status(404).send(e) + res.status(404).send(e); } } - }) + }); - router.get(/^\/(.+)\/did.json$/, async (req: RequestWithAgentDIDManager, res) => { - if (req.agent) { - try { - const identifier = await req.agent.didManagerGet({ - did: 'did:web:' + getAliasForRequest(req) + ':' + req.params[0].replace(/\//g, ':'), - }) - const didDoc = didDocForIdentifier(identifier) - res.json(didDoc) - } catch (e) { - res.status(404).send(e) + router.get( + /^\/(.+)\/did.json$/, + async (req: RequestWithAgentDIDManager, res) => { + if (req.agent) { + try { + const identifier = await req.agent.didManagerGet({ + did: + 'did:web:' + + getAliasForRequest(req) + + ':' + + req.params[0].replace(/\//g, ':'), + }); + const didDoc = didDocForIdentifier(identifier); + res.json(didDoc); + } catch (e) { + res.status(404).send(e); + } } } - }) + ); + + return router; +}; + +/** + * Converts a hex string to a multibase encoded string + * The sourcecode is copied from https://github.com/uport-project/veramo/pull/1082/files# + */ + +const MULTIBASE_BASE58BTC_PREFIX = 'z'; +const MULTICODEC_PREFIX = [0xed, 0x01]; - return router +function hexToMultibase(hexString: string): string { + const hexBytes = u8a.fromString(hexString, 'hex'); + const modifiedKey = u8a.concat([MULTICODEC_PREFIX, hexBytes]); + return `${MULTIBASE_BASE58BTC_PREFIX}${u8a.toString( + modifiedKey, + 'base58btc' + )}`; } diff --git a/packages/vc-api/README.md b/packages/vc-api/README.md index 8ee2e03a..f5c91ab5 100644 --- a/packages/vc-api/README.md +++ b/packages/vc-api/README.md @@ -1,39 +1,94 @@ -# vc-api agent router +# vc-api Router -- This agent router conform to vc-api standard that to achieve interoperability's goal between various parties. +This repository contains an agent router that adheres to the vc-api standard, aiming to achieve interoperability between various parties. ## Usage -- This plugin follow the `veramo` architecture , so you can configure it with `agent.yml` +The router follows the `veramo` architecture, allowing you to configure it using the `agent.yml` file. Below is an example of how to set up the router with different functionalities: -```jsx +```yaml # API base path - - - /issuer - - $require: '@vckit/vc-api-issuer?t=function#AgentRouter' - $args: - - createCredential: createVerifiableCredential - updateCredentialStatus: updateVerifiableCredentialStatus +- - $require: '@vckit/vc-api?t=function#HolderRouter' +- - $require: '@vckit/vc-api?t=function#IssuerRouter' + $args: + - createCredential: createVerifiableCredential + updateCredentialStatus: updateVerifiableCredentialStatus +- - $require: '@vckit/vc-api?t=function#VerifierRouter' + $args: + - verifyCredential: verifyCredential + verifyPresentation: verifyPresentation + +# VC API docs path +- - /vc-api.json + - $require: '@vckit/vc-api?t=function#VCApiSchemaRouter' + $args: + - basePath: :3332 + +- - /vc-api-docs + - $require: '@vckit/vc-api?t=function#VCApiDocsRouter' ``` ## Test with test-suite -- Clone the test suite: https://github.com/w3c-ccg/vc-api-issuer-test-suite -- Go to `node_modules/vc-api-test-suite-implementations/implementations` , create files except the index file -- Create new implementation file: +To test the agent router, you can use the vc-api test suite. Follow the steps below: + +1. Clone the test suite repositories: vc-api-issuer-test-suite and vc-api-verifier-test-suite. + +2. Navigate to node_modules/vc-api-test-suite-implementations/implementations, and create a new implementation file with the following content: -```jsx +```json { - "name": "GoSource", - "implementation": "GoSource Verifiable Credentials", - "issuers": [{ - "id": "YOUR_DID_MANAGED_BY_YOUR_KMS", - "endpoint": "http://localhost:3332/agent/credentials/issue", - "options": { - "type": "Ed25519Signature2020" - }, - "tags": ["vc-api", "Ed25519Signature2020"] - }] + "name": "XYZ", + "implementation": "Verifiable Credentials", + "issuers": [ + { + "id": "DID_WEB", + "endpoint": "http://localhost:3332/credentials/issue", + "method": "POST", + "tags": ["vc-api", "Ed25519Signature2020"] + } + ], + "verifiers": [ + { + "id": "DID_WEB", + "endpoint": "http://localhost:3332/credentials/verify", + "method": "POST", + "tags": ["vc-api", "Ed25519Signature2020"] + } + ] } ``` -- Run the test command `npm run test` +3. Modify the W3C credentialPlugin in `agent.yml` to use the appropriate signature suite. For example, if the test suite is using `Ed25519Signature2020`, replace `VeramoEd25519Signature2018` with `VeramoEd25519Signature2020`: + +```yaml +# W3C credentialPlugin +credentialIssuerLD: + $require: '@veramo/credential-ld#CredentialIssuerLD' + $args: + - suites: + - $require: '@veramo/credential-ld#VeramoEd25519Signature2020' + - $require: '@veramo/credential-ld#VeramoEcdsaSecp256k1RecoverySignature2020' + contextMaps: + # The LdDefaultContext is a "catch-all" for now. + - $require: '@veramo/credential-ld?t=object#LdDefaultContexts' + - $require: '@transmute/credentials-context?t=object#contexts' + # others should be included here +``` + +4. Create a `did:web` by using the API, as shown in the following example using `curl`: + +```bash +curl -X POST "http://localhost:3332/agent/didManagerCreate" \ + -H "accept: application/json; charset=utf-8"\ + -H "authorization: Bearer test123"\ + -H "content-type: application/json" \ + -d '{"alias":"ngrok_host","provider":"did:web","kms":"local","options":{"keyType":"Ed25519"}}' + +``` + +Replace `ngrok_host` with the host of your ngrok tunnel on port 3332. + +5. Update the implementation file with your newly created `did:web`. + +6. Run the test command: `npm run test`. diff --git a/packages/vc-api/__tests__/holder-controller.test.ts b/packages/vc-api/__tests__/holder-controller.test.ts new file mode 100644 index 00000000..682a8ec3 --- /dev/null +++ b/packages/vc-api/__tests__/holder-controller.test.ts @@ -0,0 +1,535 @@ +import { + deleteCredential, + deletePresentation, + deriveCredential, + getCredential, + getCredentials, + getExchanges, + getPresentation, + getPresentations, + initiateExchange, + provePresentation, + receiveExchange, +} from '../src/controllers/holder-controller'; +import { Request, Response } from 'express'; +import { RequestWithAgent } from '../src/types/request-type'; +import { IAgent } from '@vckit/core-types'; +import { jest } from '@jest/globals'; + +describe('getCredential', () => { + let mockRequest: Partial; + let mockResponse: Response; + + beforeEach(() => { + const agent = { + execute: jest.fn(async (method: string, args: any) => { + return Promise.resolve([ + { verifiableCredential: 'test credential' }, + ] as any); + }), + } as unknown as IAgent; + mockRequest = { + agent, + params: { + id: 'test-id', + }, + }; + mockResponse = { + status: jest.fn((code: number) => mockResponse), + json: jest.fn((data: any) => mockResponse), + } as unknown as Response; + }); + + it('should return the verifiable credential when it exists', async () => { + await getCredential( + mockRequest as RequestWithAgent, + mockResponse as Response + ); + + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.json).toHaveBeenCalledWith('test credential'); + }); + + it('should return a 404 error when the credential is not found', async () => { + (mockRequest.agent!.execute as jest.Mock).mockResolvedValue([]); + + await getCredential(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(404); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: 'Credential not found', + }); + }); + + it('should return an error when the agent is not available', async () => { + mockRequest.agent = undefined; + try { + await getCredential(mockRequest as Request, mockResponse as Response); + } catch (e) { + expect(e.message).toEqual('Agent not available'); + } + }); + + it('should return an error when an exception occurs', async () => { + const error = new Error('Some error'); + (mockRequest.agent!.execute as jest.Mock).mockRejectedValue(error); + + await getCredential(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(500); // or another appropriate error code + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Some error' }); + }); +}); + +describe('getCredentials', () => { + let mockRequest: Partial; + let mockResponse: Response; + + beforeEach(() => { + const agent = { + execute: jest.fn(async (method: string, args: any) => { + return Promise.resolve([ + { verifiableCredential: 'test credential' }, + ] as any); + }), + } as unknown as IAgent; + mockRequest = { + agent, + params: {}, + }; + mockResponse = { + status: jest.fn((code: number) => mockResponse), + json: jest.fn((data: any) => mockResponse), + } as unknown as Response; + }); + + it('should return the verifiable credentials when they exist', async () => { + await getCredentials( + mockRequest as RequestWithAgent, + mockResponse as Response + ); + + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.json).toHaveBeenCalledWith(['test credential']); + }); + + it('should return a 410 error when there are no credentials', async () => { + (mockRequest.agent!.execute as jest.Mock).mockResolvedValue([]); + + await getCredentials(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(410); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: 'Gone! There is no data here', + }); + }); + + it('should return an error when the agent is not available', async () => { + mockRequest.agent = undefined; + try { + await getCredentials(mockRequest as Request, mockResponse as Response); + } catch (e) { + expect(e.message).toEqual('Agent not available'); + } + }); + + it('should return an error when an exception occurs', async () => { + const error = new Error('Some error'); + (mockRequest.agent!.execute as jest.Mock).mockRejectedValue(error); + + await getCredentials(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(500); // or another appropriate error code + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Some error' }); + }); +}); + +describe('deleteCredential', () => { + let mockRequest: Partial; + let mockResponse: Response; + + beforeEach(() => { + const agent = { + execute: jest.fn(async (method: string, args: any) => { + if (method === 'dataStoreDeleteVerifiableCredential') { + return Promise.resolve(true as any); + } + return Promise.resolve([ + { verifiableCredential: 'test credential' }, + ] as any); + }), + } as unknown as IAgent; + mockRequest = { + agent, + params: {}, + }; + mockResponse = { + status: jest.fn((code: number) => mockResponse), + json: jest.fn((data: any) => mockResponse), + } as unknown as Response; + }); + + it('should return a 202 when the credential is deleted', async () => { + await deleteCredential( + mockRequest as RequestWithAgent, + mockResponse as Response + ); + + expect(mockResponse.status).toHaveBeenCalledWith(202); + }); + + it('should return a 404 error when the credential is not found', async () => { + (mockRequest.agent!.execute as jest.Mock) = jest.fn( + async (method: string, args: any) => { + if (method === 'dataStoreDeleteVerifiableCredential') { + return Promise.resolve(false as any); + } + return Promise.resolve([ + { verifiableCredential: 'test credential' }, + ] as any); + } + ); + + await deleteCredential(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(404); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: 'Credential not found', + }); + }); + + it('should return an error when the agent is not available', async () => { + mockRequest.agent = undefined; + try { + await deleteCredential(mockRequest as Request, mockResponse as Response); + } catch (e) { + expect(e.message).toEqual('Agent not available'); + } + }); + + it('should return an error when an exception occurs', async () => { + const error = new Error('Some error'); + (mockRequest.agent!.execute as jest.Mock).mockRejectedValue(error); + + await deleteCredential(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(500); // or another appropriate error code + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Some error' }); + }); +}); + +describe('getPresentation', () => { + let mockRequest: Partial; + let mockResponse: Response; + + beforeEach(() => { + const agent = { + execute: jest.fn(async (method: string, args: any) => { + return Promise.resolve([ + { verifiablePresentation: 'test presentation' }, + ] as any); + }), + } as unknown as IAgent; + mockRequest = { + agent, + params: {}, + }; + mockResponse = { + status: jest.fn((code: number) => mockResponse), + json: jest.fn((data: any) => mockResponse), + } as unknown as Response; + }); + + it('should return the verifiable presentation when it exists', async () => { + await getPresentation( + mockRequest as RequestWithAgent, + mockResponse as Response + ); + + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.json).toHaveBeenCalledWith('test presentation'); + }); + + it('should return a 404 error when there are no presentations', async () => { + (mockRequest.agent!.execute as jest.Mock).mockResolvedValue([]); + + await getPresentation(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(404); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: 'Presentation not found', + }); + }); + + it('should return an error when the agent is not available', async () => { + mockRequest.agent = undefined; + try { + await getPresentation(mockRequest as Request, mockResponse as Response); + } catch (e) { + expect(e.message).toEqual('Agent not available'); + } + }); + + it('should return an error when an exception occurs', async () => { + const error = new Error('Some error'); + (mockRequest.agent!.execute as jest.Mock).mockRejectedValue(error); + + await getPresentation(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(500); // or another appropriate error code + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Some error' }); + }); +}); + +describe('getPresentations', () => { + let mockRequest: Partial; + let mockResponse: Response; + + beforeEach(() => { + const agent = { + execute: jest.fn(async (method: string, args: any) => { + return Promise.resolve([ + { verifiablePresentation: 'test presentation' }, + ] as any); + }), + } as unknown as IAgent; + mockRequest = { + agent, + params: {}, + }; + mockResponse = { + status: jest.fn((code: number) => mockResponse), + json: jest.fn((data: any) => mockResponse), + } as unknown as Response; + }); + + it('should return the verifiable presentations when they exist', async () => { + await getPresentations( + mockRequest as RequestWithAgent, + mockResponse as Response + ); + + expect(mockResponse.status).toHaveBeenCalledWith(200); + expect(mockResponse.json).toHaveBeenCalledWith(['test presentation']); + }); + + it('should return a 410 error when there are no presentations', async () => { + (mockRequest.agent!.execute as jest.Mock).mockResolvedValue([]); + + await getPresentations(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(410); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: 'Gone! There is no data here', + }); + }); + + it('should return an error when the agent is not available', async () => { + mockRequest.agent = undefined; + try { + await getPresentations(mockRequest as Request, mockResponse as Response); + } catch (e) { + expect(e.message).toEqual('Agent not available'); + } + }); + + it('should return an error when an exception occurs', async () => { + const error = new Error('Some error'); + (mockRequest.agent!.execute as jest.Mock).mockRejectedValue(error); + + await getPresentations(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(500); // or another appropriate error code + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Some error' }); + }); +}); + +describe('provePresentation', () => { + let mockRequest: Partial; + let mockResponse: Response; + + beforeEach(() => { + const agent = { + execute: jest.fn(async (method: string, args: any) => { + return Promise.resolve('test presentation' as any); + }), + } as unknown as IAgent; + mockRequest = { + agent, + body: { presentation: {}, options: {} }, + }; + mockResponse = { + status: jest.fn((code: number) => mockResponse), + json: jest.fn((data: any) => mockResponse), + } as unknown as Response; + }); + + it('should return the verifiable presentation when proving an presentation', async () => { + await provePresentation( + mockRequest as RequestWithAgent, + mockResponse as Response + ); + + expect(mockResponse.status).toHaveBeenCalledWith(201); + expect(mockResponse.json).toHaveBeenCalledWith('test presentation'); + }); + + it('should return an error when the agent is not available', async () => { + mockRequest.agent = undefined; + try { + await provePresentation(mockRequest as Request, mockResponse as Response); + } catch (e) { + expect(e.message).toEqual('Agent not available'); + } + }); + + it('should return an error when an exception occurs', async () => { + const error = new Error('Some error'); + (mockRequest.agent!.execute as jest.Mock).mockRejectedValue(error); + + await provePresentation(mockRequest as Request, mockResponse as Response); + + expect(mockResponse.status).toHaveBeenCalledWith(500); // or another appropriate error code + expect(mockResponse.json).toHaveBeenCalledWith({ error: 'Some error' }); + }); +}); + +describe('deletePresentation', () => { + let mockRequest: Partial; + let mockResponse: Response; + + beforeEach(() => { + const agent = {} as unknown as IAgent; + mockRequest = { + agent, + }; + mockResponse = { + status: jest.fn((code: number) => mockResponse), + json: jest.fn((data: any) => mockResponse), + } as unknown as Response; + }); + + it('should return a 501 when deleting an presentation is not implemented', async () => { + await deletePresentation( + mockRequest as RequestWithAgent, + mockResponse as Response + ); + + expect(mockResponse.status).toHaveBeenCalledWith(501); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: 'Not implemented', + }); + }); +}); + +describe('deriveCredential', () => { + let mockRequest: Partial; + let mockResponse: Response; + + beforeEach(() => { + const agent = {} as unknown as IAgent; + mockRequest = { + agent, + }; + mockResponse = { + status: jest.fn((code: number) => mockResponse), + json: jest.fn((data: any) => mockResponse), + } as unknown as Response; + }); + + it('should return a 501 when deriving a credential is not implemented', async () => { + await deriveCredential( + mockRequest as RequestWithAgent, + mockResponse as Response + ); + + expect(mockResponse.status).toHaveBeenCalledWith(501); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: 'Not implemented', + }); + }); +}); + +describe('getExchanges', () => { + let mockRequest: Partial; + let mockResponse: Response; + + beforeEach(() => { + const agent = {} as unknown as IAgent; + mockRequest = { + agent, + }; + mockResponse = { + status: jest.fn((code: number) => mockResponse), + json: jest.fn((data: any) => mockResponse), + } as unknown as Response; + }); + + it('should return a 501 when getting exchanges is not implemented', async () => { + await getExchanges( + mockRequest as RequestWithAgent, + mockResponse as Response + ); + + expect(mockResponse.status).toHaveBeenCalledWith(501); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: 'Not implemented', + }); + }); +}); + +describe('initiateExchange', () => { + let mockRequest: Partial; + let mockResponse: Response; + + beforeEach(() => { + const agent = {} as unknown as IAgent; + mockRequest = { + agent, + }; + mockResponse = { + status: jest.fn((code: number) => mockResponse), + json: jest.fn((data: any) => mockResponse), + } as unknown as Response; + }); + + it('should return a 501 when initiating an exchange is not implemented', async () => { + await initiateExchange( + mockRequest as RequestWithAgent, + mockResponse as Response + ); + + expect(mockResponse.status).toHaveBeenCalledWith(501); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: 'Not implemented', + }); + }); +}); + +describe('receiveExchange', () => { + let mockRequest: Partial; + let mockResponse: Response; + + beforeEach(() => { + const agent = {} as unknown as IAgent; + mockRequest = { + agent, + }; + mockResponse = { + status: jest.fn((code: number) => mockResponse), + json: jest.fn((data: any) => mockResponse), + } as unknown as Response; + }); + + it('should return a 501 when receiving an exchange is not implemented', async () => { + await receiveExchange( + mockRequest as RequestWithAgent, + mockResponse as Response + ); + + expect(mockResponse.status).toHaveBeenCalledWith(501); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: 'Not implemented', + }); + }); +}); diff --git a/packages/vc-api/package.json b/packages/vc-api/package.json index be8b3f99..530d5801 100644 --- a/packages/vc-api/package.json +++ b/packages/vc-api/package.json @@ -15,7 +15,8 @@ "debug": "^4.3.3", "did-resolver": "^4.0.1", "express": "^4.18.2", - "express-validator": "^7.0.1" + "express-openapi-validator": "^5.0.4", + "yaml": "^2.1.3" }, "devDependencies": { "@types/debug": "4.1.7", diff --git a/packages/vc-api/src/config/index.ts b/packages/vc-api/src/config/index.ts index e5a1488d..2d6bd53a 100644 --- a/packages/vc-api/src/config/index.ts +++ b/packages/vc-api/src/config/index.ts @@ -3,11 +3,13 @@ import { ProofFormat } from '@vckit/core-types'; export type IssuerConfiguration = { proofFormat: ProofFormat; removeOriginalFields: boolean; - save: boolean + save: boolean; + + [x: string]: any; }; export const configuration: IssuerConfiguration = { - proofFormat: 'OpenAttestationMerkleProofSignature2018', // The proof format required by the schema. Default to OpenAttestationMerkleProofSignature2018. + proofFormat: 'lds', // The proof format only supports 'lds' at the moment. removeOriginalFields: false, // Remove payload members during JWT-JSON transformation. Defaults to `true`. diff --git a/packages/vc-api/src/controllers/holder-controller.ts b/packages/vc-api/src/controllers/holder-controller.ts new file mode 100644 index 00000000..b6ea77a5 --- /dev/null +++ b/packages/vc-api/src/controllers/holder-controller.ts @@ -0,0 +1,303 @@ +import { + IAgent, + UniqueVerifiableCredential, + UniqueVerifiablePresentation, + UnsignedPresentation, + VerifiableCredential, + VerifiablePresentation, +} from '@vckit/core-types'; +import { Response } from 'express'; +import { errorHandler } from '../error-handler.js'; +import { RequestWithAgent } from '../types/request-type.js'; +import { configuration } from '../config/index.js'; + +/** + * Retrieves a specific verifiable credential based on its ID. + * @public + * @param {RequestWithAgent} req - The request object. + * @param {Response} res - The response object. + * @throws {Error} If the agent is not available. + */ +export const getCredential = async (req: RequestWithAgent, res: Response) => { + if (!req.agent) throw Error('Agent not available'); + try { + const result = await getUniqueVerifiableCredential( + req.agent, + req.params.id + ); + if (!result) { + res.status(404).json({ error: 'Credential not found' }); + } else { + res.status(200).json(result.verifiableCredential); + } + } catch (e) { + const error = errorHandler(e); + res.status(error.code).json({ error: error.message }); + } +}; + +/** + * Retrieves all verifiable credentials. + * @public + * @param {RequestWithAgent} req - The request object. + * @param {Response} res - The response object. + * @throws {Error} If the agent is not available. + */ +export const getCredentials = async (req: RequestWithAgent, res: Response) => { + if (!req.agent) throw Error('Agent not available'); + try { + const result: Array = await getVerifiableCredentials( + req.agent + ); + if (result.length === 0) { + res.status(410).json({ error: 'Gone! There is no data here' }); + } else { + res.status(200).json(result); + } + } catch (e) { + const error = errorHandler(e); + res.status(error.code).json({ error: error.message }); + } +}; + +/** + * Deletes a specific verifiable credential based on its ID. + * @public + * @param {RequestWithAgent} req - The request object. + * @param {Response} res - The response object. + * @throws {Error} If the agent is not available. + */ +export const deleteCredential = async ( + req: RequestWithAgent, + res: Response +) => { + if (!req.agent) throw Error('Agent not available'); + try { + const result = await deleteVerifiableCredential(req.agent, req.params.id); + if (!result) { + res.status(404).json({ error: 'Credential not found' }); + } else { + res.status(202).json({ message: 'Credential deleted' }); + } + } catch (e) { + const error = errorHandler(e); + res.status(error.code).json({ error: error.message }); + } +}; +/** + * Derives a verifiable credential is not implemented. + * @param {RequestWithAgent} req - The request object. + * @param {Response} res - The response object. + * @throws {Error} If the agent is not available. + */ +export const deriveCredential = async ( + req: RequestWithAgent, + res: Response +) => { + if (!req.agent) throw Error('Agent not available'); + res.status(501).json({ error: 'Not implemented' }); +}; + +/** + * Retrieves a specific verifiable presentation based on its ID. + * @public + * @param {RequestWithAgent} req - The request object. + * @param {Response} res - The response object. + * @throws {Error} If the agent is not available. + */ +export const getPresentation = async (req: RequestWithAgent, res: Response) => { + if (!req.agent) throw Error('Agent not available'); + try { + const result = await getUniqueVerifiablePresentation( + req.agent, + req.params.id + ); + if (!result) { + res.status(404).json({ error: 'Presentation not found' }); + } else { + res.status(200).json(result.verifiablePresentation); + } + } catch (e) { + const error = errorHandler(e); + res.status(error.code).json({ error: error.message }); + } +}; + +/** + * Retrieves all verifiable presentations. + * @public + * @param {RequestWithAgent} req - The request object. + * @param {Response} res - The response object. + * @throws {Error} If the agent is not available. + */ +export const getPresentations = async ( + req: RequestWithAgent, + res: Response +) => { + if (!req.agent) throw Error('Agent not available'); + try { + const result = await getVerifiablePresentations(req.agent); + if (result.length === 0) { + res.status(410).json({ error: 'Gone! There is no data here' }); + } else { + res.status(200).json(result); + } + } catch (e) { + const error = errorHandler(e); + res.status(error.code).json({ error: error.message }); + } +}; + +/** + * Proves a verifiable presentation. + * @public + * @param {RequestWithAgent} req - The request object. + * @param {Response} res - The response object. + * @throws {Error} If the agent is not available. + */ +export const provePresentation = async ( + req: RequestWithAgent, + res: Response +) => { + if (!req.agent) throw Error('Agent not available'); + try { + const result = await proveVerifiablefPresentation(req.agent, req.body); + res.status(201).json(result); + } catch (e) { + const error = errorHandler(e); + res.status(error.code).json({ error: error.message }); + } +}; + +/** + * delete a verifiable presentation is not implemented. + * @param {RequestWithAgent} req - The request object. + * @param {Response} res - The response object. + * @throws {Error} If the agent is not available. + */ +export const deletePresentation = async ( + req: RequestWithAgent, + res: Response +) => { + if (!req.agent) throw Error('Agent not available'); + res.status(501).json({ error: 'Not implemented' }); +}; + +/** + * get exchanges is not implemented. + * @param {RequestWithAgent} req - The request object. + * @param {Response} res - The response object. + * @throws {Error} If the agent is not available. + */ +export const getExchanges = async (req: RequestWithAgent, res: Response) => { + if (!req.agent) throw Error('Agent not available'); + res.status(501).json({ error: 'Not implemented' }); +}; + +/** + * initiate an exchange is not implemented. + * @param {RequestWithAgent} req - The request object. + * @param {Response} res - The response object. + * @throws {Error} If the agent is not available. + */ +export const initiateExchange = async ( + req: RequestWithAgent, + res: Response +) => { + if (!req.agent) throw Error('Agent not available'); + res.status(501).json({ error: 'Not implemented' }); +}; + +/** + * receive the exchange is not implemented. + * @param {RequestWithAgent} req - The request object. + * @param {Response} res - The response object. + * @throws {Error} If the agent is not available. + */ +export const receiveExchange = async (req: RequestWithAgent, res: Response) => { + if (!req.agent) throw Error('Agent not available'); + res.status(501).json({ error: 'Not implemented' }); +}; + +const getUniqueVerifiableCredential = async ( + agent: IAgent, + id: string +): Promise => { + const params = { + where: [{ column: 'id', value: [id], not: false, op: 'Equal' }], + }; + const result: Array = await agent.execute( + 'dataStoreORMGetVerifiableCredentials', + params + ); + return result[0] ? result[0] : null; +}; + +const deleteVerifiableCredential = async ( + agent: IAgent, + id: string +): Promise => { + const credential = await getUniqueVerifiableCredential(agent, id); + if (!credential) throw Error('not_found: Credential not found'); + + const params = { + hash: credential.hash, + }; + return agent.execute('dataStoreDeleteVerifiableCredential', params); +}; + +const getVerifiableCredentials = async ( + agent: IAgent +): Promise => { + const params = {}; + const result = await agent.execute( + 'dataStoreORMGetVerifiableCredentials', + params + ); + return result.map( + (credential: UniqueVerifiableCredential) => credential.verifiableCredential + ); +}; + +const getUniqueVerifiablePresentation = async ( + agent: IAgent, + id: string +): Promise => { + const params = { + where: [{ column: 'id', value: [id], not: false, op: 'Equal' }], + }; + const result: Array = await agent.execute( + 'dataStoreORMGetVerifiablePresentations', + params + ); + return result[0] ? result[0] : null; +}; + +const getVerifiablePresentations = async (agent: IAgent) => { + const params = {}; + const result = await agent.execute( + 'dataStoreORMGetVerifiablePresentations', + params + ); + return result.map( + (presentation: UniqueVerifiablePresentation) => + presentation.verifiablePresentation + ); +}; + +const proveVerifiablefPresentation = async ( + agent: IAgent, + data: { presentation: UnsignedPresentation; options: any } +): Promise => { + const params = { + presentation: data.presentation, + ...configuration, + ...data.options, + }; + + const result: VerifiablePresentation = await agent.execute( + 'createVerifiablePresentation', + params + ); + return result; +}; diff --git a/packages/vc-api/src/error-handler.ts b/packages/vc-api/src/error-handler.ts new file mode 100644 index 00000000..3cea18f7 --- /dev/null +++ b/packages/vc-api/src/error-handler.ts @@ -0,0 +1,36 @@ +/** + * Defines the structure of a VC API error. + */ +interface VCApiError { + code: number; + message: string; + data?: any; +} + +/** + * Handles and transforms an error into a VCApiError object. + * @param {Error} error - The error to be handled. + * @returns {VCApiError} The transformed VCApiError object. + */ +export const errorHandler = (error: Error): VCApiError => { + let vcApiError: VCApiError; + const errorType = error.message.split(':')[0]; + const errorMessage = error.message.split(':')[1]?.trim(); + switch (errorType) { + case 'not_found': + return (vcApiError = { + code: 404, + message: errorMessage, + }); + case 'not_implemented': + return (vcApiError = { + code: 501, + message: errorMessage, + }); + default: + return (vcApiError = { + code: 500, + message: error.message, + }); + } +}; diff --git a/packages/vc-api/src/holder-router.ts b/packages/vc-api/src/holder-router.ts new file mode 100644 index 00000000..b37ba79f --- /dev/null +++ b/packages/vc-api/src/holder-router.ts @@ -0,0 +1,62 @@ +import { Router, json } from 'express'; +import { + deleteCredential, + deletePresentation, + deriveCredential, + getCredential, + getCredentials, + getExchanges, + getPresentation, + getPresentations, + initiateExchange, + provePresentation, + receiveExchange, +} from './controllers/holder-controller.js'; +import { validatorMiddleware } from './validator-middleware.js'; + +/** + * + * Creates a router for handling holder-related routes. + * @public + * @returns {Router} The Express router configured for holder routes. + */ +export const HolderRouter = (): Router => { + const router = Router(); + router.use(json({ limit: '10mb' })); + + router.get('/credentials/:id', validatorMiddleware(), getCredential); + + router.get('/credentials', validatorMiddleware(), getCredentials); + + router.delete('/credentials/:id', validatorMiddleware(), deleteCredential); + + router.post('/credentials/derive', validatorMiddleware(), deriveCredential); + + router.get('/presentations/:id', validatorMiddleware(), getPresentation); + + router.get('/presentations', validatorMiddleware(), getPresentations); + + router.post('/presentations/prove', validatorMiddleware(), provePresentation); + + router.delete( + '/presentations/:id', + validatorMiddleware(), + deletePresentation + ); + + router.get('/exchanges', validatorMiddleware(), getExchanges); + + router.post( + '/exchanges/:exchangeId', + validatorMiddleware(), + initiateExchange + ); + + router.post( + '/exchanges/:exchangeId/:transactionId', + validatorMiddleware(), + receiveExchange + ); + + return router; +}; diff --git a/packages/vc-api/src/index.ts b/packages/vc-api/src/index.ts index 70da2d76..936e9a2d 100644 --- a/packages/vc-api/src/index.ts +++ b/packages/vc-api/src/index.ts @@ -1 +1,7 @@ -export { IssuerRouter, IssuerRouterOptions } from './issuer-router.js' + +export { VerifierRouter, VerifierRouterOptions } from './verifier-router.js' +export { IssuerRouter, IssuerRouterOptions } from './issuer-router.js'; +export { HolderRouter } from './holder-router.js'; +export { VCApiSchemaRouter } from './vc-api-schema-router.js'; +export { VCApiDocsRouter } from './vc-api-docs-router.js'; + \ No newline at end of file diff --git a/packages/vc-api/src/issuer-router.ts b/packages/vc-api/src/issuer-router.ts index 14b64e69..b5705036 100644 --- a/packages/vc-api/src/issuer-router.ts +++ b/packages/vc-api/src/issuer-router.ts @@ -1,10 +1,15 @@ import { IAgent, VerifiableCredential } from '@vckit/core-types'; import { Request, Response, NextFunction, Router, json } from 'express'; -import { validateCredentialPayload, validateUpdateStatusCredentialPayload } from './middlewares/index.js'; -import { IssueCredentialRequestPayload, UpdateCredentialStatusRequestPayload } from './types/index.js'; -import { mapCredentialPayload, mapCredentialResponse } from './utils/index.js'; -import { configuration } from './config/index.js'; -import { validationResult } from 'express-validator'; +import { + IssueCredentialRequestPayload, + UpdateCredentialStatusRequestPayload, +} from './types/index.js'; +import { mapCredentialPayload } from './utils/index.js'; +import { + IssuerConfiguration, + configuration as DEFAULT_CONFIG, +} from './config/index.js'; +import { validatorMiddleware } from './validator-middleware.js'; interface RequestWithAgent extends Request { agent?: IAgent; @@ -15,7 +20,7 @@ interface RequestWithAgent extends Request { */ export interface IssuerRouterOptions { /** - * Agaent method to create credential + * Agent method to create credential */ createCredential: string; @@ -23,6 +28,10 @@ export interface IssuerRouterOptions { * Agent method to update credential status (revocation) */ updateCredentialStatus: string; + + config?: IssuerConfiguration; + + apiSpec: string; } /** @@ -37,20 +46,15 @@ export interface IssuerRouterOptions { export const IssuerRouter = ({ createCredential, updateCredentialStatus, + config, }: IssuerRouterOptions): Router => { const router = Router(); router.use(json({ limit: '10mb' })); router.post( '/credentials/issue', - validateCredentialPayload(), + validatorMiddleware(), async (req: RequestWithAgent, res: Response, next: NextFunction) => { - const errors = validationResult(req); - - if (!errors.isEmpty()) { - return res.status(400).json({ description: errors.array() }); - } - if (!req.agent) { throw Error('Agent not available'); } @@ -58,29 +62,23 @@ export const IssuerRouter = ({ try { const payload = mapCredentialPayload( req.body as IssueCredentialRequestPayload, - configuration + { ...DEFAULT_CONFIG, ...config } ); - const result = (await req.agent.execute( + const verifiableCredential = (await req.agent.execute( createCredential, payload )) as VerifiableCredential; - res.status(201).json(mapCredentialResponse(result)); + res.status(201).json({ verifiableCredential }); } catch (e: any) { - return res.status(400).json({ error: e.message }); + return res.status(500).json({ error: e.message }); } } ); router.post( '/credentials/status', - validateUpdateStatusCredentialPayload(), + validatorMiddleware(), async (req: RequestWithAgent, res: Response, next: NextFunction) => { - const errors = validationResult(req); - - if (!errors.isEmpty()) { - return res.status(400).json({ description: errors.array() }); - } - if (!req.agent) { throw Error('Agent not available'); } diff --git a/packages/vc-api/src/middlewares/index.ts b/packages/vc-api/src/middlewares/index.ts deleted file mode 100644 index 535f132a..00000000 --- a/packages/vc-api/src/middlewares/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './issuer.js' diff --git a/packages/vc-api/src/middlewares/issuer.ts b/packages/vc-api/src/middlewares/issuer.ts deleted file mode 100644 index a2b2da6b..00000000 --- a/packages/vc-api/src/middlewares/issuer.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { check, oneOf } from 'express-validator'; - -/** - * Validate the input for issuing verifiable credential that use vc-kit. - */ - -export const validateCredentialPayload = () => { - return [ - check('credential').isObject().notEmpty(), - ...validateContext(), - ...validateIssuer(), - ...validateCredentialSubject(), - ...validateType(), - ...validateOptions(), - ]; -}; - -export const validateUpdateStatusCredentialPayload = () => { - return [ - check('credentialId', 'credentialId must be a string') - .isString() - .notEmpty(), - check('credentialStatus', 'credentialStatus must be an array').isArray({ - min: 1, - }), - check('credentialStatus.*.type', 'credentialStatus.type must be a string') - .isString() - .notEmpty(), - check( - 'credentialStatus.*.status', - 'credentialStatus.status must be a string' - ) - .isString() - .notEmpty(), - ]; -}; - -const validateContext = () => { - return [ - oneOf( - [ - check('credential.@context').isArray({ min: 1 }), - check('credential.@context').isString().notEmpty(), - ], - { message: 'credential.@context must be an array or string' } - ), - ]; -}; - -const validateIssuer = () => { - return [ - oneOf( - [ - check('credential.issuer').isObject().notEmpty(), - check('credential.issuer').isString().notEmpty(), - ], - { message: 'credential.issuer must be string or an object' } - ), - ]; -}; - -const validateCredentialSubject = () => { - return [ - check( - 'credential.credentialSubject', - 'credential.credentialSubject must be an object' - ) - .isObject() - .notEmpty(), - ]; -}; - -const validateType = () => { - return [ - oneOf( - [ - check('credential.type').isArray({ min: 1 }), - check('credential.type').isString().notEmpty(), - ], - { message: 'credential.type must be an array or string' } - ), - ]; -}; - -const validateOptions = () => { - return [ - check('options.created', 'options.created must be a string') - .optional() - .isString() - .notEmpty(), - check('options.challenge', 'options.created must be a string') - .optional() - .isString() - .notEmpty(), - check('options.credentialStatus', 'options.created must be an object') - .optional() - .isObject() - .notEmpty(), - check( - 'options.credentialStatus.type', - 'options.credentialStatus.type must be a string' - ) - .optional() - .isObject() - .notEmpty(), - ]; -}; diff --git a/packages/vc-api/src/types/issuer.ts b/packages/vc-api/src/types/issuer.ts index a05467e9..20841609 100644 --- a/packages/vc-api/src/types/issuer.ts +++ b/packages/vc-api/src/types/issuer.ts @@ -6,7 +6,7 @@ export type IssueCredentialRequestPayload = { options?: IssueCredentialOptions; // Options for specifying how the LinkedDataProof is created. }; -type IssueCredentialOptions = { +export type IssueCredentialOptions = { created?: string; // The date and time of the proof. Default current system time. credentialStatus?: { type: string }; // The method of credential status to issue the credential including. If omitted credential status will be included. challenge?: string; // A challenge provided by the requesting party of the proof. For example 6e62f66e-67de-11eb-b490-ef3eeefa55f2 diff --git a/packages/vc-api/src/types/request-type.ts b/packages/vc-api/src/types/request-type.ts new file mode 100644 index 00000000..318be2cb --- /dev/null +++ b/packages/vc-api/src/types/request-type.ts @@ -0,0 +1,9 @@ +import { IAgent } from '@vckit/core-types'; +import { Request } from 'express'; + +/** + * @public + */ +export interface RequestWithAgent extends Request { + agent?: IAgent; +} diff --git a/packages/vc-api/src/types/verifier.ts b/packages/vc-api/src/types/verifier.ts new file mode 100644 index 00000000..f49189ca --- /dev/null +++ b/packages/vc-api/src/types/verifier.ts @@ -0,0 +1,31 @@ +import { VerifiableCredential, VerifiablePresentation } from '@vckit/core-types'; +import { IssueCredentialOptions } from './issuer'; + +export type VerifierCredentialRequestPayload = { + verifiableCredential: VerifiableCredential; + options?: VerifierCredentialOptions; // Options for specifying how the LinkedDataProof is created. +}; + +export type VerifierCredentialOptions = Omit< + IssueCredentialOptions, + 'credentialStatus' | 'created' +>; + +export type VerificationResult = { + checks: string[]; + warnings: string[]; + errors: string[]; +} + +export type VerifierPresentationRequestPayload = { + verifiablePresentation: VerifiablePresentation; + options?: VerifierPresentationOptions; +} + +export type VerifierPresentationOptions = { + verificationMethod?: string; + proofPurpose?: string; + domain?: string; + created?: string; + challenge?: string; +} \ No newline at end of file diff --git a/packages/vc-api/src/utils/mapping.ts b/packages/vc-api/src/utils/mapping.ts index 0b5f7794..b63b84d3 100644 --- a/packages/vc-api/src/utils/mapping.ts +++ b/packages/vc-api/src/utils/mapping.ts @@ -1,12 +1,16 @@ import { ICreateVerifiableCredentialArgs, - VerifiableCredential, + IVerifyCredentialArgs, + IVerifyPresentationArgs, } from '@vckit/core-types'; import { IssuerConfiguration } from '../config'; import { - IssueCredentialRequestPayload, - IssueCredentialResponsePayload, + IssueCredentialRequestPayload } from '../types'; +import { + VerifierCredentialRequestPayload, + VerifierPresentationRequestPayload, +} from '../types/verifier'; export const mapCredentialPayload = ( payload: IssueCredentialRequestPayload, @@ -17,14 +21,28 @@ export const mapCredentialPayload = ( return { credential: payload.credential, ...configuration, - ...options + ...options, }; }; -export const mapCredentialResponse = ( - verifiableCredential: VerifiableCredential -): IssueCredentialResponsePayload => { +export const mapVerifiableCredentialPayload = ( + payload: VerifierCredentialRequestPayload +): IVerifyCredentialArgs => { + const options = payload.options || {}; + + return { + credential: payload.verifiableCredential, + ...options, + }; +}; + +export const mapVerifiablePresentationPayload = ( + payload: VerifierPresentationRequestPayload +): IVerifyPresentationArgs => { + const options = payload.options || {}; + return { - verifiableCredential, + presentation: payload.verifiablePresentation, + ...options, }; }; diff --git a/packages/vc-api/src/validator-middleware.ts b/packages/vc-api/src/validator-middleware.ts new file mode 100644 index 00000000..49daf463 --- /dev/null +++ b/packages/vc-api/src/validator-middleware.ts @@ -0,0 +1,18 @@ +import path from 'path'; +import * as OpenApiValidator from 'express-openapi-validator'; +import { Router } from 'express'; + +export const validatorMiddleware = (): Router => { + const router = Router(); + router.use( + OpenApiValidator.middleware({ + apiSpec: path.join( + path.resolve(), + 'packages/vc-api/src/vc-api-schemas/vc-api.yaml' + ), + validateRequests: true, + validateResponses: false, + }) + ); + return router; +}; diff --git a/packages/vc-api/src/vc-api-docs-router.ts b/packages/vc-api/src/vc-api-docs-router.ts new file mode 100644 index 00000000..7008f736 --- /dev/null +++ b/packages/vc-api/src/vc-api-docs-router.ts @@ -0,0 +1,40 @@ +import { Router } from 'express'; +import { RequestWithAgent } from './types/request-type.js'; + +/** + * Creates a router that exposes vc api documentation browser + * + * @returns Expressjs router + * + * @public + */ +export const VCApiDocsRouter = (): Router => { + const router = Router(); + const rapidoc = ` + + + + + + + + + + + + `; + router.get('/', (req: RequestWithAgent, res) => { + res.status(200); + res.set('Content-Type', 'text/html'); + res.send(Buffer.from(rapidoc)); + }); + + return router; +}; diff --git a/packages/vc-api/src/vc-api-schema-router.ts b/packages/vc-api/src/vc-api-schema-router.ts new file mode 100644 index 00000000..a77df22f --- /dev/null +++ b/packages/vc-api/src/vc-api-schema-router.ts @@ -0,0 +1,43 @@ +import { Router } from 'express'; +import YAML from 'yaml'; +import fs from 'fs'; +import { RequestWithAgent } from './types/request-type.js'; + +/** + * @public + */ +export interface ApiSchemaRouterOptions { + /** + * Base path + */ + basePath: string; +} + +/** + * Creates a router that exposes vc-api OpenAPI schema + * + * @param options - Initialization option + * @returns Expressjs router + * + * @public + */ +export const VCApiSchemaRouter = (options: ApiSchemaRouterOptions): Router => { + const router = Router(); + + router.get('/', (req: RequestWithAgent, res) => { + const file = fs.readFileSync( + 'packages/vc-api/src/vc-api-schemas/vc-api.yaml', + 'utf8' + ); + const schema = YAML.parse(file); + const url = + (req.headers['x-forwarded-proto'] || req.protocol) + + '://' + + req.hostname + + options.basePath; + schema.servers = [{ url }]; + res.json(schema); + }); + + return router; +}; diff --git a/packages/vc-api/src/vc-api-schemas/vc-api.yaml b/packages/vc-api/src/vc-api-schemas/vc-api.yaml new file mode 100644 index 00000000..293c388d --- /dev/null +++ b/packages/vc-api/src/vc-api-schemas/vc-api.yaml @@ -0,0 +1,1061 @@ +openapi: 3.0.0 +info: + version: 0.0.3-unstable + title: VC Holder API + description: >- + This is an Experimental Open API Specification for the [VC Data + Model](https://www.w3.org/TR/vc-data-model/). + license: + name: W3C Software and Document License + url: http://www.w3.org/Consortium/Legal/copyright-software. + contact: + name: GitHub Source Code + url: https://github.com/w3c-ccg/vc-api +tags: + - name: holder_Credentials + x-displayName: Credentials + - name: holder_Presentations + x-displayName: Presentations + - name: holder_Exchanges + x-displayName: Exchanges + - name: issuer_Credentials + x-displayName: Credentials + - name: verifier_Credentials + x-displayName: Credentials + - name: verifier_Presentations + x-displayName: Presentations +paths: + /credentials/{id}: + get: + tags: + - holder_Credentials + security: + - oAuth2: [] + summary: Gets a credential or verifiable credential by ID + operationId: getCredential + parameters: + - $ref: '#/components/parameters/ObjectId' + responses: + '200': + description: Credential retrieved + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Credential' + - $ref: '#/components/schemas/VerifiableCredential' + '400': + description: Bad Request + '401': + description: Not Authorized + '404': + description: Credential not found + '410': + description: Gone! There is no data here + '418': + description: >- + I'm a teapot - MUST not be returned outside of pre-arranged + scenarios between both parties + '500': + description: Internal Error + '501': + description: Not Implemented + delete: + tags: + - holder_Credentials + security: + - oAuth2: [] + summary: Deletes a credential or verifiable credential by ID + operationId: deleteCredential + parameters: + - $ref: '#/components/parameters/ObjectId' + responses: + '202': + description: >- + Credential deleted - this is a 202 by default as soft deletes and + processing time are assumed + '400': + description: Bad Request + '401': + description: Not Authorized + '404': + description: Credential not found + '410': + description: Gone! There is no data here + '500': + description: Internal Error + '501': + description: Not Implemented + /credentials: + get: + tags: + - holder_Credentials + security: + - oAuth2: [] + summary: Gets list of credentials or verifiable credentials + operationId: getCredentials + parameters: + - in: query + name: type + schema: + type: array + items: + type: string + pattern: (credentials|verifiablecredentials|all) + responses: + '200': + description: Credentials retrieved + content: + application/json: + schema: + type: array + description: The Credentials + items: + anyOf: + - $ref: '#/components/schemas/VerifiableCredential' + - $ref: '#/components/schemas/Credential' + '400': + description: Bad Request + '401': + description: Not Authorized + '410': + description: Gone! There is no data here + '500': + description: Internal Error + '501': + description: Not Implemented + /credentials/derive: + post: + tags: + - holder_Credentials + security: + - oAuth2: [] + summary: Derives a credential and returns it in the response body. + operationId: deriveCredential + description: Derives a credential and returns it in the response body. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DeriveCredentialRequest' + description: Parameters for deriving the credential. + responses: + '201': + description: Credential derived successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/VerifiableCredential' + '400': + description: Invalid Request + '500': + description: Internal Error + '501': + description: Not Implemented + /presentations/{id}: + get: + tags: + - holder_Presentations + summary: Gets a presentation or verifiable presentation by ID + security: + - oAuth2: [] + operationId: getPresentation + parameters: + - $ref: '#/components/parameters/ObjectId' + responses: + '200': + description: Credential retrieved + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Presentation' + - $ref: '#/components/schemas/VerifiablePresentation' + '400': + description: Bad Request + '401': + description: Not Authorized + '404': + description: Presentation not found + '410': + description: Gone! There is no data here + '500': + description: Internal Error + '501': + description: Not Implemented + delete: + tags: + - holder_Presentations + summary: Deletes a presentation or verifiable presentation by ID + security: + - oAuth2: [] + operationId: deletePresentation + parameters: + - $ref: '#/components/parameters/ObjectId' + responses: + '202': + description: >- + Presentation deleted - this is a 202 by default as soft deletes and + processing time are assumed + '400': + description: Bad Request + '401': + description: Not Authorized + '404': + description: Presentation not found + '410': + description: Gone! There is no data here + '500': + description: Internal Error + '501': + description: Not Implemented + /presentations: + get: + tags: + - holder_Presentations + summary: Gets list of presentations or verifiable presentations + security: + - oAuth2: [] + operationId: getPresentations + parameters: + - in: query + name: type + schema: + type: array + items: + type: string + pattern: (presentations|verifiablepresentations|all) + responses: + '200': + description: Presentations retrieved + content: + application/json: + schema: + type: array + description: The Presentations + items: + anyOf: + - $ref: '#/components/schemas/Presentation' + - $ref: '#/components/schemas/VerifiablePresentation' + '400': + description: Bad Request + '401': + description: Not Authorized + '410': + description: Gone! There is no data here + '500': + description: Internal Error + '501': + description: Not Implemented + /presentations/prove: + post: + summary: Proves a presentation and returns it in the response body. + tags: + - holder_Presentations + security: + - oAuth2: [] + operationId: provePresentation + description: Proves a presentation and returns it in the response body. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProvePresentationRequest' + description: Parameters for proving the presentation. + responses: + '201': + description: Presentation successfully proved! + content: + application/json: + schema: + $ref: '#/components/schemas/ProvePresentationResponse' + '400': + description: invalid input! + '500': + description: error! + /exchanges: + get: + summary: >- + Provides a discovery endpoint for the exchanges supported by this server + endpoint. + tags: + - holder_Exchanges + security: + - oAuth2: [] + operationId: discoverExchanges + description: >- + This endpoint returns an array of the exchange-ids (path endpoints) + supported by this server, and the associated protocol supported by that + exchange endpoint. The list supports pagination. Clients consuming this + list wishing to use an exchange endpoint MUST recognize and support the + protocol identified in the value field. Clients are not expected to + dynamically process the protocol specified. + parameters: + - name: index + in: query + description: >- + The starting index for the list that is meaningful to the server. If + omitted the server must assume the start of the list. + required: false + schema: + type: string + - name: limit + in: query + description: >- + The maximum number of items to return in the response. If omitted + the service should return all remaining items from the start index. + required: false + schema: + type: number + responses: + '200': + description: >- + A map of the exchange-id endpoints to protocols those exchanges + support. + content: + application/json: + schema: + required: + - count + - index + - total + - exchanges + properties: + count: + type: number + description: The number of elements returned in the array. + total: + type: number + description: The total number of elements available. + exchanges: + type: array + items: + type: object + properties: + id: + type: string + description: >- + the path name of the exchange endpoint. May be a + UUID. + type: + type: string + description: >- + MUST be a string that references the supported + protocol on that endpoint. + index: + type: object + properties: + self: + type: string + description: >- + The index position of the start of the returned list. + Examples could be a numerica value, a URL, or a value + meaningful to the server. + next: + type: string + description: >- + The index position for the next set of results (ie, + index of the end of this list). Examples could be a + numerica value, a URL, or a value meaningful to the + server. + '400': + description: invalid input + '500': + description: error + /exchanges/{exchange-id}: + post: + summary: Initiates an exchange of information. + tags: + - holder_Exchanges + security: + - oAuth2: [] + operationId: initiateExchange + description: >- + A client can use this endpoint to initiate an exchange of a particular + type. The client can include HTTP POST information related to the + details of exchange it would like to initiate. If the server understands + the request, it returns a Verifiable Presentation Request. A request + that the server cannot understand results in an error. + parameters: + - $ref: '#/components/parameters/ExchangeId' + requestBody: + description: >- + Information related to the type of exchange the client would like to + start. + content: + application/json: + schema: + anyOf: + - type: object + description: Data necessary to initiate the exchange. + - $ref: '#/components/schemas/NotifyPresentationAvailableRequest' + responses: + '200': + description: Proceed with exchange. + content: + application/json: + schema: + $ref: '#/components/schemas/VerifiablePresentationRequestBody' + '400': + description: Request is malformed. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error. + '501': + description: Service not implemented. + /exchanges/{exchange-id}/{transaction-id}: + post: + summary: Receives information related to an existing exchange. + tags: + - holder_Exchanges + security: + - oAuth2: [] + operationId: receiveExchangeData + description: >- + A client can use this endpoint to continue the exchange of information + associated with an initiated exchange by sending a Verifiable + Presentation with information requested by the server to this endpoint. + parameters: + - $ref: '#/components/parameters/ExchangeId' + - $ref: '#/components/parameters/TransactionId' + requestBody: + description: A Verifiable Presentation. + content: + application/json: + schema: + $ref: '#/components/schemas/VerifiablePresentationBody' + responses: + '200': + description: Received data was accepted. + content: + application/json: + schema: + anyOf: + - $ref: '#/components/schemas/VerifiablePresentationBody' + - $ref: '#/components/schemas/VerifiablePresentationRequestBody' + '400': + description: Received data is malformed. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: The associated exchange or transaction was not found. + '500': + description: Internal server error. + '501': + description: Service not implemented. + /credentials/issue: + post: + summary: Issues a credential and returns it in the response body. + tags: + - issuer_Credentials + security: + - oAuth2: [] + operationId: issueCredential + description: Issues a credential and returns it in the response body. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/IssueCredentialRequest' + description: Parameters for issuing the credential. + responses: + '201': + description: Credential successfully issued! + content: + application/json: + schema: + $ref: '#/components/schemas/IssueCredentialResponse' + '400': + description: invalid input! + '500': + description: error! + /credentials/status: + post: + summary: Updates the status of an issued credential + tags: + - issuer_Credentials + security: + - networkAuth: [] + - oAuth2: [] + - zCap: [] + operationId: updateCredentialStatus + description: Updates the status of an issued credential. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateCredentialStatus' + description: Parameters for updating the status of the issued credential. + responses: + '200': + description: Credential status successfully updated + '400': + description: Bad Request + '404': + description: Credential not found + '500': + description: Internal Server Error + /credentials/verify: + post: + summary: >- + Verifies a verifiableCredential and returns a verificationResult in the + response body. + tags: + - verifier_Credentials + security: + - networkAuth: [] + - oAuth2: [] + - zCap: [] + operationId: verifyCredential + description: >- + Verifies a verifiableCredential and returns a verificationResult in the + response body. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/VerifyCredentialRequest' + description: Parameters for verifying a verifiableCredential. + responses: + '200': + description: Verifiable Credential successfully verified! + content: + application/json: + schema: + $ref: '#/components/schemas/VerificationResult' + '400': + description: invalid input! + '500': + description: error! + /presentations/verify: + post: + summary: >- + Verifies a Presentation with or without proofs attached and returns a + verificationResult in the response body. + tags: + - verifier_Presentations + security: + - networkAuth: [] + - oAuth2: [] + - zCap: [] + operationId: verifyPresentation + description: >- + Verifies a verifiablePresentation and returns a verificationResult in + the response body. Given the possibility of denial of service, buffer + overflow, or other style attacks, an implementation is permitted to rate + limit or restrict requests against this API endpoint to those requests + that contain only a single credential with a 413 or 429 error code as + appropriate. + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/VerifyPresentationRequest' + - $ref: '#/components/schemas/ProoflessVerifyPresentationRequest' + description: Parameters for verifying a verifiablePresentation. + responses: + '200': + description: Verifiable Presentation successfully verified! + content: + application/json: + schema: + $ref: '#/components/schemas/VerificationResult' + '400': + description: Invalid or malformed input + '413': + description: Payload too large + '429': + description: Request rate limit exceeded. + '500': + description: Internal Server Error +components: + # securitySchemes: + # oAuth2: + # type: http + # scheme: bearer + # bearerFormat: VcapiOauth2 + schemas: + DeriveCredentialRequest: + type: object + properties: + verifiableCredential: + $ref: '#/components/schemas/VerifiableCredential' + frame: + type: object + description: A JSON-LD frame used for selective disclosure. + options: + $ref: '#/components/schemas/DeriveCredentialOptions' + DeriveCredentialResponse: + $ref: '#/components/schemas/VerifiableCredential' + ProvePresentationRequest: + type: object + properties: + presentation: + $ref: '#/components/schemas/Presentation' + options: + $ref: '#/components/schemas/PresentCredentialOptions' + ProvePresentationResponse: + type: object + properties: + verifiablePresentation: + $ref: '#/components/schemas/VerifiablePresentation' + NotifyPresentationAvailableRequest: + type: object + properties: + query: + type: object + description: See https://w3c-ccg.github.io/vp-request-spec/#format + properties: + type: + type: string + description: The type of query the server should reply with. + credentialQuery: + type: object + description: Details of the client's available presentation + NotifyPresentationAvailableResponse: + type: object + properties: + query: + type: object + description: See https://w3c-ccg.github.io/vp-request-spec/#format + domain: + type: string + description: See https://w3id.org/security#domain + challenge: + type: string + description: See https://w3id.org/security#challenge + ErrorResponse: + type: object + description: A response that denotes that an error has occurred. + properties: + id: + type: string + pattern: '[a-z0-9\-]{8,}' + description: An error id. + message: + type: string + minLength: 10 + maxLength: 100 + description: The error message. + details: + type: object + description: An object with error details. + required: + - id + - message + StorePresentationRequest: + $ref: '#/components/schemas/VerifiablePresentation' + VerifiablePresentationRequestBody: + type: object + properties: + verifiablePresentationRequest: + $ref: '#/components/schemas/VerifiablePresentationRequest' + VerifiablePresentationBody: + type: object + properties: + verifiablePresentation: + $ref: '#/components/schemas/VerifiablePresentation' + Issuer: + description: A JSON-LD Verifiable Credential Issuer. + oneOf: + - type: string + - type: object + properties: + id: + type: string + description: The issuer id. + Credential: + type: object + description: A JSON-LD Verifiable Credential without a proof. + required: + - '@context' + - issuer + - credentialSubject + - type + properties: + '@context': + type: array + description: The JSON-LD context of the credential. + items: + type: string + id: + type: string + description: The ID of the credential. + type: + type: array + description: The JSON-LD type of the credential. + minItems: 1 + items: + type: string + issuer: + $ref: '#/components/schemas/Issuer' + issuanceDate: + type: string + description: The issuanceDate + expirationDate: + type: string + description: The expirationDate + credentialSubject: + type: object + description: The subject + LinkedDataProof: + type: object + description: A JSON-LD Linked Data proof. + required: + - type + - created + - verificationMethod + - proofPurpose + - proofValue + properties: + type: + type: string + description: Linked Data Signature Suite used to produce proof. + created: + type: string + description: Date the proof was created. + challenge: + type: string + description: >- + A value chosen by the verifier to mitigate authentication proof + replay attacks. + domain: + type: string + description: The domain of the proof to restrict its use to a particular target. + nonce: + type: string + description: >- + A value chosen by the creator of a proof to randomize proof values + for privacy purposes. + verificationMethod: + type: string + description: Verification Method used to verify proof. + proofPurpose: + type: string + description: The purpose of the proof to be used with verificationMethod. + jws: + type: string + description: Detached JSON Web Signature. + proofValue: + type: string + description: Value of the Linked Data proof. + VerifiableCredential: + type: object + description: A JSON-LD Verifiable Credential with a proof. + allOf: + - $ref: '#/components/schemas/Credential' + - type: object + required: + - proof + properties: + proof: + $ref: '#/components/schemas/LinkedDataProof' + DeriveCredentialOptions: + type: object + additionalProperties: false + description: Options for specifying how the derived credential is created. + properties: + nonce: + type: string + description: >- + An encoded nonce provided by the holder of the credential to be + included into the LinkedDataProof. + Presentation: + type: object + description: A JSON-LD Verifiable Presentation without a proof. + properties: + '@context': + type: array + description: The JSON-LD context of the presentation. + items: + type: string + id: + type: string + description: The ID of the presentation. + type: + type: array + description: The JSON-LD type of the presentation. + items: + type: string + holder: + type: object + description: >- + The holder - will be ignored if no proof is present since there is + no proof of authority over the credentials + nullable: true + verifiableCredential: + type: array + description: The Verifiable Credentials + items: + $ref: '#/components/schemas/VerifiableCredential' + VerifiablePresentation: + type: object + description: A JSON-LD Verifiable Presentation with a proof. + allOf: + - $ref: '#/components/schemas/Presentation' + - type: object + required: + - proof + properties: + proof: + $ref: '#/components/schemas/LinkedDataProof' + PresentCredentialOptions: + type: object + additionalProperties: false + description: Options for specifying how the LinkedDataProof is created. + properties: + type: + type: string + description: >- + The type of the proof. Default is an appropriate proof type + corresponding to the verification method. + verificationMethod: + type: string + description: >- + The URI of the verificationMethod used for the proof. If omitted, a + default verification method will be used. + proofPurpose: + type: string + description: The purpose of the proof. Default 'assertionMethod'. + created: + type: string + description: >- + The date and time of the proof (with a maximum accuracy in seconds). + Default current system time. + challenge: + type: string + description: >- + A challenge provided by the requesting party of the proof. For + example 6e62f66e-67de-11eb-b490-ef3eeefa55f2 + domain: + type: string + description: >- + The intended domain of validity for the proof. For example + website.example + VerifiablePresentationRequest: + type: object + description: A Verifiable Presentation Request. + properties: + query: + type: array + description: A set of one or more queries sent by the requester. + items: + type: object + properties: + type: + type: array + description: The type of the query. + items: + type: string + challenge: + type: string + description: >- + A challenge, intended to prevent replay attacks, provided by the + requester that is typically expected to be included in the + Verifiable Presentation response. + domain: + type: string + description: >- + A domain, intended to prevent replay attacks, provided by the + requester that is typically expected to be included in the + Verifiable Presentation response. + items: + type: string + interact: + type: array + description: A list of interaction mechanisms that are supported by the server. + items: + type: object + properties: + service: + type: object + description: >- + A service that is supported by the server that is capable of + receiving a response to the Verifiable Presentation Request. + properties: + type: + type: array + description: The type of the service. + items: + type: string + serviceEndpoint: + type: string + description: >- + A URL that can be utilized for interacting with the + service for the purposes of responding to the Verifiable + Presentation Request. + IssueCredentialRequest: + type: object + required: + - credential + properties: + credential: + $ref: '#/components/schemas/Credential' + options: + $ref: '#/components/schemas/IssueCredentialOptions' + IssueCredentialResponse: + type: object + properties: + verifiableCredential: + $ref: '#/components/schemas/VerifiableCredential' + UpdateCredentialStatus: + type: object + description: Request for updating the status of an issued credential. + properties: + credentialId: + type: string + credentialStatus: + type: array + items: + type: object + properties: + type: + type: string + status: + type: string + IssueCredentialOptions: + type: object + additionalProperties: false + description: Options for specifying how the LinkedDataProof is created. + properties: + created: + type: string + description: >- + The date and time of the proof (with a maximum accuracy in seconds). + Default current system time. + challenge: + type: string + description: >- + A challenge provided by the requesting party of the proof. For + example 6e62f66e-67de-11eb-b490-ef3eeefa55f2 + domain: + type: string + description: >- + The intended domain of validity for the proof. For example + website.example + credentialStatus: + type: object + description: >- + The method of credential status to issue the credential including. + If omitted credential status will be included. + properties: + type: + type: string + description: The type of credential status to issue the credential with + VerifyCredentialRequest: + type: object + properties: + verifiableCredential: + $ref: '#/components/schemas/VerifiableCredential' + options: + $ref: '#/components/schemas/VerifyOptions' + VerifyCredentialResponse: + $ref: '#/components/schemas/VerificationResult' + VerifyPresentationRequest: + type: object + properties: + verifiablePresentation: + $ref: '#/components/schemas/VerifiablePresentation' + options: + $ref: '#/components/schemas/VerifyOptions' + ProoflessVerifyPresentationRequest: + type: object + properties: + presentation: + $ref: '#/components/schemas/Presentation' + VerifyPresentationResponse: + $ref: '#/components/schemas/VerificationResult' + VerifyOptions: + type: object + additionalProperties: false + description: Options for specifying how the LinkedDataProof is verified. + properties: + checks: + type: array + items: + type: string + description: An array of verification checks to be performed on the credential. WARNING - Implementers are cautioned that the list of checks is currently incomplete and other checks such as issuance, expiration, nonce, domain, and acceptable issuers are expected to be added in time. A default list is also expected to encapsulate all available checks. This option might be changed to a 'ignore' option where it lists the checks the developer would like to skip. + example: ['proof'] + challenge: + type: string + description: >- + A challenge provided by the requesting party of the proof. For + example 6e62f66e-67de-11eb-b490-ef3eeefa55f2 + domain: + type: string + description: >- + The intended domain of validity for the proof. For example + website.example + VerificationResult: + type: object + additionalProperties: false + description: Object summarizing a verification + properties: + checks: + type: array + description: The checks performed + items: + type: string + warnings: + type: array + description: Warnings + items: + type: string + errors: + type: array + description: Errors + items: + type: string + parameters: + ObjectId: + in: path + name: id + required: true + schema: + anyOf: + - type: string + pattern: "[0-9a-f]{8}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{12}" + - type: string + pattern: z[1-9A-HJ-NP-Za-km-z]{21,22} + - type: string + pattern: u[a-zA-Z0-9_-]{22,23} + ExchangeId: + name: exchange-id + description: A potentially human-readable identifier for an exchange. + in: path + required: true + schema: + type: string + minimum: 3 + pattern: '[a-z0-9][a-z0-9\-]{2,}' + TransactionId: + in: path + name: transaction-id + required: true + schema: + anyOf: + - type: string + pattern: "[0-9a-f]{8}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{12}" + - type: string + pattern: z[1-9A-HJ-NP-Za-km-z]{21,22} + - type: string + pattern: u[a-zA-Z0-9_-]{22,23} +x-tagGroups: + - name: holder + tags: + - holder_Credentials + - holder_Presentations + - holder_Exchanges + - name: issuer + tags: + - issuer_Credentials + - name: verifier + tags: + - verifier_Credentials + - verifier_Presentations diff --git a/packages/vc-api/src/verifier-router.ts b/packages/vc-api/src/verifier-router.ts new file mode 100644 index 00000000..789a1b77 --- /dev/null +++ b/packages/vc-api/src/verifier-router.ts @@ -0,0 +1,99 @@ +import { IAgent, IVerifyResult } from '@vckit/core-types'; +import { Request, Response, NextFunction, Router, json } from 'express'; +import { + mapVerifiableCredentialPayload, + mapVerifiablePresentationPayload, +} from './utils/index.js'; +import { + VerifierCredentialRequestPayload, + VerifierPresentationRequestPayload, +} from './types/verifier.js'; +import { validatorMiddleware } from './validator-middleware.js'; + +interface RequestWithAgent extends Request { + agent?: IAgent; +} + +/** + * @public + */ +export interface VerifierRouterOptions { + /** + * Agent method to verify credential + */ + verifyCredential: string; + + /** + * Agent method to verify presentation + */ + verifyPresentation: string; + + apiSpec: string; +} + +/** + * Creates a router that conform to vc-api interfaces for verifier. + * + * + * @param options - Initialization option + * @returns Expressjs router + * + * @public + */ +export const VerifierRouter = ({ + verifyCredential, + verifyPresentation, +}: VerifierRouterOptions): Router => { + const router = Router(); + router.use(json({ limit: '10mb' })); + + router.post( + '/credentials/verify', + validatorMiddleware(), + async (req: RequestWithAgent, res: Response, next: NextFunction) => { + if (!req.agent) { + throw Error('Agent not available'); + } + + try { + const payload = mapVerifiableCredentialPayload( + req.body as VerifierCredentialRequestPayload + ); + + const result = (await req.agent.execute( + verifyCredential, + payload + )) as IVerifyResult; + res.status(200).json(result); + } catch (e: any) { + return res.status(500).json({ error: e.message }); + } + } + ); + + router.post( + '/presentations/verify', + validatorMiddleware(), + async (req: RequestWithAgent, res: Response, next: NextFunction) => { + if (!req.agent) { + throw Error('Agent not available'); + } + + try { + const payload = mapVerifiablePresentationPayload( + req.body as VerifierPresentationRequestPayload + ); + + const result = (await req.agent.execute( + verifyPresentation, + payload + )) as IVerifyResult; + res.status(200).json(result); + } catch (e: any) { + return res.status(500).json({ error: e.message }); + } + } + ); + + return router; +};