From d40ba75600b3dadd07bff6ecc423000023f3d958 Mon Sep 17 00:00:00 2001 From: nklomp Date: Sat, 25 Feb 2023 01:55:02 +0100 Subject: [PATCH] feat: New QR code provider plugin. Can generate both SIOPv2 and DIDCommv2 OOB QRs. Support for text generation and React QR codes as SVG --- packages/waci-pex-qr-react/README.md | 30 ++--- .../waci-pex-qr-react/__tests__/agent.yml | 6 +- .../__tests__/encoding.test.ts | 48 +------ .../__tests__/shared/fixtures.tsx | 85 ++++++++++++ .../shared/ssiQrCodeProviderLogic.tsx | 58 ++++---- packages/waci-pex-qr-react/package.json | 27 ++-- .../src/agent/QrCodeProvider.tsx | 82 ++++++++++++ .../src/agent/WaciQrCodeProvider.tsx | 32 ----- .../src/agent/qr-utils/outOfBandMessage.tsx | 21 --- .../src/agent/utils/ReactQr.tsx | 58 ++++++++ .../agent/utils/didCommOutOfBandMessage.tsx | 12 ++ .../src/agent/utils/index.ts | 2 + packages/waci-pex-qr-react/src/index.ts | 4 +- .../src/types/IQRCodeGenerator.ts | 125 ++++++++++++++++++ .../waci-pex-qr-react/src/types/WaciTypes.ts | 75 ----------- 15 files changed, 436 insertions(+), 229 deletions(-) create mode 100644 packages/waci-pex-qr-react/__tests__/shared/fixtures.tsx create mode 100644 packages/waci-pex-qr-react/src/agent/QrCodeProvider.tsx delete mode 100644 packages/waci-pex-qr-react/src/agent/WaciQrCodeProvider.tsx delete mode 100644 packages/waci-pex-qr-react/src/agent/qr-utils/outOfBandMessage.tsx create mode 100644 packages/waci-pex-qr-react/src/agent/utils/ReactQr.tsx create mode 100644 packages/waci-pex-qr-react/src/agent/utils/didCommOutOfBandMessage.tsx create mode 100644 packages/waci-pex-qr-react/src/agent/utils/index.ts create mode 100644 packages/waci-pex-qr-react/src/types/IQRCodeGenerator.ts delete mode 100644 packages/waci-pex-qr-react/src/types/WaciTypes.ts diff --git a/packages/waci-pex-qr-react/README.md b/packages/waci-pex-qr-react/README.md index 0e3af2186..8c1a65dbf 100644 --- a/packages/waci-pex-qr-react/README.md +++ b/packages/waci-pex-qr-react/README.md @@ -39,7 +39,7 @@ recipient to either: 1. authenticate itself to the requester 2. inviting the issuer to issue a credential -The data fields required to generate the QR code will depend on the type of request and the acceptable values. The +The object fields required to generate the QR code will depend on the type of request and the acceptable values. The possible `accept` value may be: 1. `oidc4vp` @@ -50,10 +50,10 @@ possible `accept` value may be: #### Importing the plugin ```typescript -import { WaciQrCodeProvider } from '@sphereon/ssi-sdk-waci-pex-qr-react' +import { QrCodeProvider } from '@sphereon/ssi-sdk-waci-pex-qr-react' // Include in the interface -// const agent = createAgent<... WaciQrCodeProvider> +// const agent = createAgent<... QrCodeProvider> ``` #### Adding plugin to the agent @@ -61,7 +61,7 @@ import { WaciQrCodeProvider } from '@sphereon/ssi-sdk-waci-pex-qr-react' ```typescript plugins: [ ... - new WaciQrCodeProvider() + new QrCodeProvider() ], ``` @@ -81,19 +81,19 @@ import { QRContent, QRType } from '@sphereon/ssi-sdk-waci-pex-qr-react' #### Inside the component we can declare or get the values to pass to QR Code plugin ```typescript -import { OobPayload } from '@sphereon/ssi-sdk-waci-pex-qr-react' +import { WaciOobProps } from '@sphereon/ssi-sdk-waci-pex-qr-react' -function getOobQrCodeProps(): OobQRProps { +function getOobQrCodeProps(): QRRenderingProps { return { - oobBaseUrl: 'https://example.com/?oob=', - type: QRType.DID_AUTH_SIOP_V2, + baseUrl: 'https://example.com/?oob=', + type: QRType.SIOPV2, id: '599f3638-b563-4937-9487-dfe55099d900', from: 'did:key:zrfdjkgfjgfdjk', - body: { + object: { goalCode: GoalCode.STREAMLINED_VP, accept: [AcceptMode.SIOPV2_WITH_OIDC4VP], }, - onGenerate: (oobQRProps: OobQRProps, payload: OobPayload) => { + onGenerate: (oobQRProps: QRRenderingProps, payload: WaciOobProps) => { console.log(payload) }, bgColor: 'white', @@ -105,7 +105,7 @@ function getOobQrCodeProps(): OobQRProps { } delegateCreateOobQRCode = () => { - let qrCode = createOobQrCode(this.getOobQrCodeProps()) + let qrCode = createQrCode(this.getOobQrCodeProps()) return qrCode.then((qrCodeResolved) => { return qrCodeResolved }) @@ -119,7 +119,7 @@ On generate gives the following (example) output "type": "openid", "id": "599f3638-b563-4937-9487-dfe55099d900", "from": "did:key:zrfdjkgfjgfdjk", - "body": { + "object": { "goal-code": "streamlined-vp", "accept": ["siopv2+oidc4vp"] } @@ -129,9 +129,9 @@ On generate gives the following (example) output If you want to create the payload manually and want to do serialization yourself you can use: ```typescript -const payload = OutOfBandMessage.createPayload(getOobQrCodeProps()) -const encoded = OutOfBandMessage.urlEncode(payload) -const url = oobQRProps.oobBaseUrl + encoded +const payload = DidCommOutOfBandMessage.createPayload(getOobQrCodeProps()) +const encoded = DidCommOutOfBandMessage.urlEncode(payload) +const url = oobQRProps.baseUrl + encoded console.log(url) // https://example.com/?oob=eyJ0eXBlIjoic2lvcHYyIiwiaWQiOiI1OTlmMzYzOC1iNTYzLTQ5MzctOTQ4Ny1kZmU1NTA5OWQ5MDAiLCJmcm9tIjoiZGlkOmtleTp6cmZkamtnZmpnZmRqayIsImJvZHkiOnsiZ29hbC1jb2RlIjoic3RyZWFtbGluZWQtdnAiLCJhY2NlcHQiOlsic2lvcHYyK29pZGM0dnAiXX19 ``` diff --git a/packages/waci-pex-qr-react/__tests__/agent.yml b/packages/waci-pex-qr-react/__tests__/agent.yml index 0d32e54eb..d530cfb37 100644 --- a/packages/waci-pex-qr-react/__tests__/agent.yml +++ b/packages/waci-pex-qr-react/__tests__/agent.yml @@ -6,6 +6,6 @@ agent: $args: - schemaValidation: false plugins: - - $require: ./packages/waci-pex-qr-react/dist#WaciQrCodeProvider - $args: - - oobQRProps + - $require: ./packages/waci-pex-qr-react/dist#QrCodeProvider +# $args: +# - renderingProps diff --git a/packages/waci-pex-qr-react/__tests__/encoding.test.ts b/packages/waci-pex-qr-react/__tests__/encoding.test.ts index c528628be..2828f7c17 100644 --- a/packages/waci-pex-qr-react/__tests__/encoding.test.ts +++ b/packages/waci-pex-qr-react/__tests__/encoding.test.ts @@ -1,56 +1,22 @@ -import { AcceptMode, GoalCode, OobPayload, OobQRProps, QRType } from '../src' -import { OutOfBandMessage } from '../src/agent/qr-utils/outOfBandMessage' import base64url from 'base64url' - -const oobQRProps: OobQRProps = { - oobBaseUrl: 'https://example.com/?oob=', - type: QRType.DID_AUTH_SIOP_V2, - id: '599f3638-b563-4937-9487-dfe55099d900', - from: 'did:key:zrfdjkgfjgfdjk', - body: { - goalCode: GoalCode.STREAMLINED_VP, - accept: [AcceptMode.SIOPV2_WITH_OIDC4VP], - }, - onGenerate: (oobQRProps: OobQRProps, payload: OobPayload) => { - console.log(payload) - }, - bgColor: 'white', - fgColor: 'black', - level: 'L', - size: 128, - title: 'title2021120903', -} +import { DidCommOutOfBandMessage } from '../src/agent/utils' +import { oobInvitation } from './shared/fixtures' describe('SSI QR Code', () => { - it('should create payload object', async () => { - const payload = OutOfBandMessage.createPayload(oobQRProps) - // const encoded = OutOfBandMessage.encode(payload) - // const url = oobQRProps.oobBaseUrl + encoded - - expect(payload).toMatchObject({ - body: { accept: ['siopv2+oidc4vp'], goalCode: 'streamlined-vp' }, - from: 'did:key:zrfdjkgfjgfdjk', - id: '599f3638-b563-4937-9487-dfe55099d900', - type: 'siopv2', - }) - }) - it('should create json value', async () => { - const payload = OutOfBandMessage.createPayload(oobQRProps) - const json = OutOfBandMessage.toJson(payload) + const json = DidCommOutOfBandMessage.toJson(oobInvitation) expect(json).toMatch( - '{"type":"siopv2","id":"599f3638-b563-4937-9487-dfe55099d900","from":"did:key:zrfdjkgfjgfdjk","body":{"goal-code":"streamlined-vp","accept":["siopv2+oidc4vp"]}}' + '{"type":"https://didcomm.org/out-of-band/2.0/invitation","id":"599f3638-b563-4937-9487-dfe55099d900","from":"did:key:zrfdjkgfjgfdjk","body":{"goal_code":"streamlined-vp","accept":["didcomm/v2"]}}' ) }) it('should url encode and decode', async () => { - const payload = OutOfBandMessage.createPayload(oobQRProps) - const urlEncoded = OutOfBandMessage.urlEncode(payload) + const urlEncoded = DidCommOutOfBandMessage.urlEncode(oobInvitation) expect(urlEncoded).toMatch( - 'eyJ0eXBlIjoic2lvcHYyIiwiaWQiOiI1OTlmMzYzOC1iNTYzLTQ5MzctOTQ4Ny1kZmU1NTA5OWQ5MDAiLCJmcm9tIjoiZGlkOmtleTp6cmZkamtnZmpnZmRqayIsImJvZHkiOnsiZ29hbC1jb2RlIjoic3RyZWFtbGluZWQtdnAiLCJhY2NlcHQiOlsic2lvcHYyK29pZGM0dnAiXX19' + 'eyJ0eXBlIjoiaHR0cHM6Ly9kaWRjb21tLm9yZy9vdXQtb2YtYmFuZC8yLjAvaW52aXRhdGlvbiIsImlkIjoiNTk5ZjM2MzgtYjU2My00OTM3LTk0ODctZGZlNTUwOTlkOTAwIiwiZnJvbSI6ImRpZDprZXk6enJmZGprZ2ZqZ2ZkamsiLCJib2R5Ijp7ImdvYWxfY29kZSI6InN0cmVhbWxpbmVkLXZwIiwiYWNjZXB0IjpbImRpZGNvbW0vdjIiXX19' ) expect(base64url.decode(urlEncoded)).toMatch( - '{"type":"siopv2","id":"599f3638-b563-4937-9487-dfe55099d900","from":"did:key:zrfdjkgfjgfdjk","body":{"goal-code":"streamlined-vp","accept":["siopv2+oidc4vp"]}}' + '{"type":"https://didcomm.org/out-of-band/2.0/invitation","id":"599f3638-b563-4937-9487-dfe55099d900","from":"did:key:zrfdjkgfjgfdjk","body":{"goal_code":"streamlined-vp","accept":["didcomm/v2"]}}' ) }) }) diff --git a/packages/waci-pex-qr-react/__tests__/shared/fixtures.tsx b/packages/waci-pex-qr-react/__tests__/shared/fixtures.tsx new file mode 100644 index 000000000..f335d3514 --- /dev/null +++ b/packages/waci-pex-qr-react/__tests__/shared/fixtures.tsx @@ -0,0 +1,85 @@ +import { + CreateElementArgs, + CreateValueArgs, + DIDCommV2OOBInvitation, + DIDCommV2OOBInvitationData, + QRData, + QRRenderingProps, + QRType, + SIOPv2DataWithScheme, + ValueResult, +} from '../../src' +import { render } from '@testing-library/react' +import * as React from 'react' + +export const renderingProps: QRRenderingProps = { + bgColor: 'white', + fgColor: 'black', + level: 'L', + size: 128, + title: 'title2021120903', +} + +export const oobInvitation: DIDCommV2OOBInvitation = { + type: QRType.DIDCOMM_V2_OOB_INVITATION, + id: '599f3638-b563-4937-9487-dfe55099d900', + from: 'did:key:zrfdjkgfjgfdjk', + body: { + goal_code: 'streamlined-vp', + accept: ['didcomm/v2'], + }, +} + +export const oobInvitationData: DIDCommV2OOBInvitationData = { + oobInvitation, + baseURI: 'https://example.com/?_oob=', +} + +export const oobInvitationCreateValue: CreateValueArgs = { + data: { + object: oobInvitationData, + type: QRType.DIDCOMM_V2_OOB_INVITATION, + id: '1234', + }, + onGenerate: (result: ValueResult) => { + console.log(JSON.stringify(result, null, 2)) + }, +} + +export const oobInvitationCreateElement: CreateElementArgs = { + data: { + object: oobInvitationData, + type: QRType.DIDCOMM_V2_OOB_INVITATION, + id: '1234', + }, + renderingProps, + onGenerate: (result: ValueResult) => { + render(
{result.data.object.oobInvitation.from}
) + console.log(result.value) + }, +} + +const siopv2Object: SIOPv2DataWithScheme = { + scheme: 'openid-vc', + requestUri: 'https://test.com?id=23', +} +const siopv2Data: QRData = { + object: siopv2Object, + type: QRType.SIOPV2, + id: '456', +} +export const siopv2CreateValue: CreateValueArgs = { + data: siopv2Data, + onGenerate: (result: ValueResult) => { + console.log(JSON.stringify(result, null, 2)) + }, +} + +export const siopv2CreateElement: CreateElementArgs = { + data: siopv2Data, + renderingProps, + onGenerate: (result: ValueResult) => { + render(
{result.data.object.requestUri}
) + console.log(result.value) + }, +} diff --git a/packages/waci-pex-qr-react/__tests__/shared/ssiQrCodeProviderLogic.tsx b/packages/waci-pex-qr-react/__tests__/shared/ssiQrCodeProviderLogic.tsx index 14f0ad3b7..17b773e2e 100644 --- a/packages/waci-pex-qr-react/__tests__/shared/ssiQrCodeProviderLogic.tsx +++ b/packages/waci-pex-qr-react/__tests__/shared/ssiQrCodeProviderLogic.tsx @@ -1,30 +1,11 @@ import { TAgent } from '@veramo/core' -import { AcceptMode, GoalCode, OobPayload, OobQRProps, QRType, WaciTypes } from '../../src' +import { IQRCodeGenerator } from '../../src' import { render, screen } from '@testing-library/react' // @ts-ignore import React from 'react' +import { oobInvitationCreateElement, oobInvitationCreateValue, siopv2CreateElement, siopv2CreateValue } from './fixtures' -type ConfiguredAgent = TAgent - -const oobQRProps: OobQRProps = { - oobBaseUrl: 'https://example.com/?oob=', - type: QRType.DID_AUTH_SIOP_V2, - id: '599f3638-b563-4937-9487-dfe55099d900', - from: 'did:key:zrfdjkgfjgfdjk', - body: { - goalCode: GoalCode.STREAMLINED_VP, - accept: [AcceptMode.SIOPV2_WITH_OIDC4VP], - }, - onGenerate: (oobQRProps: OobQRProps, payload: OobPayload) => { - render(
{oobQRProps.from}
) - console.log(payload) - }, - bgColor: 'white', - fgColor: 'black', - level: 'L', - size: 128, - title: 'title2021120903', -} +type ConfiguredAgent = TAgent export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Promise; tearDown: () => Promise }) => { describe('SSI QR Code', () => { @@ -39,14 +20,16 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro await testContext.tearDown() }) - it('should create qr code', async () => { - await agent.createOobQrCode(oobQRProps).then((ssiQrCode) => { - expect(ssiQrCode).not.toBeNull() + it('should create DIDComm V2 OOB Invitation qr code value', async () => { + await agent.didCommOobInvitationValue(oobInvitationCreateValue).then((ssiQrCode) => { + expect(ssiQrCode).toEqual( + 'https://example.com/?_oob=eyJ0eXBlIjoiaHR0cHM6Ly9kaWRjb21tLm9yZy9vdXQtb2YtYmFuZC8yLjAvaW52aXRhdGlvbiIsImlkIjoiNTk5ZjM2MzgtYjU2My00OTM3LTk0ODctZGZlNTUwOTlkOTAwIiwiZnJvbSI6ImRpZDprZXk6enJmZGprZ2ZqZ2ZkamsiLCJib2R5Ijp7ImdvYWxfY29kZSI6InN0cmVhbWxpbmVkLXZwIiwiYWNjZXB0IjpbImRpZGNvbW0vdjIiXX19' + ) }) }) - it('should create qr code with props', async () => { - await agent.createOobQrCode(oobQRProps).then(async (ssiQrCode) => { + it('should create DIDComm V2 OOB Invitation qr code with renderingProps', async () => { + await agent.didCommOobInvitationElement(oobInvitationCreateElement).then(async (ssiQrCode) => { render(ssiQrCode) // The on generate created a div with test id 'test' and did:key value @@ -54,10 +37,29 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro expect(div!.childNodes[0]!.textContent).toEqual('did:key:zrfdjkgfjgfdjk') expect(ssiQrCode.props.value).toEqual( - 'https://example.com/?oob=eyJ0eXBlIjoic2lvcHYyIiwiaWQiOiI1OTlmMzYzOC1iNTYzLTQ5MzctOTQ4Ny1kZmU1NTA5OWQ5MDAiLCJmcm9tIjoiZGlkOmtleTp6cmZkamtnZmpnZmRqayIsImJvZHkiOnsiZ29hbC1jb2RlIjoic3RyZWFtbGluZWQtdnAiLCJhY2NlcHQiOlsic2lvcHYyK29pZGM0dnAiXX19' + 'https://example.com/?_oob=eyJ0eXBlIjoiaHR0cHM6Ly9kaWRjb21tLm9yZy9vdXQtb2YtYmFuZC8yLjAvaW52aXRhdGlvbiIsImlkIjoiNTk5ZjM2MzgtYjU2My00OTM3LTk0ODctZGZlNTUwOTlkOTAwIiwiZnJvbSI6ImRpZDprZXk6enJmZGprZ2ZqZ2ZkamsiLCJib2R5Ijp7ImdvYWxfY29kZSI6InN0cmVhbWxpbmVkLXZwIiwiYWNjZXB0IjpbImRpZGNvbW0vdjIiXX19' ) screen.debug() }) }) + + it('should create SIOPv2 qr code value', async () => { + await agent.siopv2Value(siopv2CreateValue).then((ssiQrCode) => { + expect(ssiQrCode).toEqual('openid-vc://?request_uri=https://test.com?id=23') + }) + }) + + it('should create SIOPv2 qr code with renderingProps', async () => { + await agent.siopv2Element(siopv2CreateElement).then(async (ssiQrCode) => { + render(ssiQrCode) + + // The on generate created a div with test id 'test' and did:key value + const div = screen.queryByTestId('test-div-siopv2') + expect(div!.childNodes[0]!.textContent).toEqual('https://test.com?id=23') + + expect(ssiQrCode.props.value).toEqual('openid-vc://?request_uri=https://test.com?id=23') + screen.debug() + }) + }) }) } diff --git a/packages/waci-pex-qr-react/package.json b/packages/waci-pex-qr-react/package.json index 9ddeae668..1ef610c05 100644 --- a/packages/waci-pex-qr-react/package.json +++ b/packages/waci-pex-qr-react/package.json @@ -1,7 +1,7 @@ { - "name": "@sphereon/ssi-sdk-waci-pex-qr-react", + "name": "@sphereon/ssi-sdk-qr-react", "version": "0.8.0", - "description": "WACI PEx QR Code provider (react)", + "description": "QR Code provider (react)", "source": "src/index.ts", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -20,18 +20,19 @@ "react-qr-code": "^2.0.11" }, "devDependencies": { - "@types/jest": "^29.2.4", - "@types/react": "^18.0.26", + "@types/jest": "^29.4.0", + "@types/react": "^18.0.28", "@types/uuid": "^8.3.4", "@veramo/cli": "4.2.0", "react-dom": "^18.2.0", - "jsdom": "^20.0.3", - "global-jsdom": "^8.6.0", - "jest-environment-jsdom": "^29.3.1", - "@testing-library/react": "^13.4.0", + "jsdom": "^21.1.0", + "global-jsdom": "^8.7.0", + "@inrupt/jest-jsdom-polyfills": "^1.5.5", + "jest-environment-jsdom": "^29.4.3", + "@testing-library/react": "^14.0.0", "@testing-library/jest-dom": "^5.16.5", - "jest": "^29.3.1", - "ts-jest": "^29.0.3", + "jest": "^29.4.3", + "ts-jest": "^29.0.5", "typescript": "^4.6.4" }, "files": [ @@ -49,9 +50,11 @@ "keywords": [ "Sphereon", "SSI", - "DIF", "React", - "WACI PEx", + "WACI", + "OpenID", + "OpenID4VP", + "OpenID4VCI", "QR Code" ] } diff --git a/packages/waci-pex-qr-react/src/agent/QrCodeProvider.tsx b/packages/waci-pex-qr-react/src/agent/QrCodeProvider.tsx new file mode 100644 index 000000000..2b5842e66 --- /dev/null +++ b/packages/waci-pex-qr-react/src/agent/QrCodeProvider.tsx @@ -0,0 +1,82 @@ +import { IAgentPlugin } from '@veramo/core' + +import { + CreateElementArgs, + CreateValueArgs, + DIDCommV2OOBInvitationData, + IQRCodeGenerator, + IRequiredContext, + QRType, + SIOPv2DataWithScheme, + URIData, +} from '../types/IQRCodeGenerator' +import { DidCommOutOfBandMessage, generateQRCodeReactElement } from './utils' +import { generateQRCodeReactElementImpl } from './utils/ReactQr' + +/** + * {@inheritDoc IQRCodeGenerator} + */ +export class QrCodeProvider implements IAgentPlugin { + readonly methods: IQRCodeGenerator = { + didCommOobInvitationElement: QrCodeProvider.didCommOobInvitationElement.bind(this), + didCommOobInvitationValue: QrCodeProvider.didCommOobInvitationValue.bind(this), + siopv2Element: QrCodeProvider.siopv2Element.bind(this), + siopv2Value: QrCodeProvider.siopv2Value.bind(this), + uriElement: QrCodeProvider.uriElement.bind(this), + } + + /** {@inheritDoc IQRCodeGenerator.uriElement} */ + + private static async uriElement(args: CreateElementArgs, context: IRequiredContext): Promise { + return generateQRCodeReactElement(args, context) + } + + /** {@inheritDoc IQRCodeGenerator.didCommOobInvitationValue} */ + private static async didCommOobInvitationValue( + args: CreateValueArgs, + context: IRequiredContext + ): Promise { + const { object } = args.data + const encoded = DidCommOutOfBandMessage.urlEncode(object.oobInvitation) + const delim = object.baseURI.includes('?') ? '&' : '?' + return object.baseURI.includes('oob=') ? object.baseURI.replace('oob=', `oob=${encoded}`) : `${object.baseURI}${delim}_oob=${encoded}` + } + + /** {@inheritDoc IQRCodeGenerator.didCommOobInvitationElement} */ + private static async didCommOobInvitationElement( + args: CreateElementArgs, + context: IRequiredContext + ): Promise { + const content = await QrCodeProvider.didCommOobInvitationValue(args, context) + return generateQRCodeReactElementImpl( + { + ...args, + data: { ...args.data, object: content }, + } as CreateElementArgs, + args, + context + ) + } + + /** {@inheritDoc IQRCodeGenerator.siopv2Value} */ + private static async siopv2Value(args: CreateValueArgs, context: IRequiredContext): Promise { + const { object } = args.data + if (typeof object === 'string') { + return object + } + + const scheme = (object.scheme ?? 'openid-vc://').replace('://?', '').replace('://', '') + '://' + const requestUri = `request_uri=${object.requestUri.replace('request_uri=', '')}` + return `${scheme}?${requestUri}` + } + + /** {@inheritDoc IQRCodeGenerator.siopv2Element} */ + private static async siopv2Element(args: CreateElementArgs, context: IRequiredContext): Promise { + const content = await QrCodeProvider.siopv2Value(args, context) + return generateQRCodeReactElementImpl( + { ...args, data: { ...args.data, object: content } } as CreateElementArgs, + args, + context + ) + } +} diff --git a/packages/waci-pex-qr-react/src/agent/WaciQrCodeProvider.tsx b/packages/waci-pex-qr-react/src/agent/WaciQrCodeProvider.tsx deleted file mode 100644 index bef45848e..000000000 --- a/packages/waci-pex-qr-react/src/agent/WaciQrCodeProvider.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { IAgentPlugin } from '@veramo/core' - -import { events, IRequiredContext, OobQRProps, WaciTypes } from '../types/WaciTypes' -import React from 'react' -import QRCode from 'react-qr-code' -import { OutOfBandMessage } from './qr-utils/outOfBandMessage' - -/** - * {@inheritDoc WaciTypes} - */ -export class WaciQrCodeProvider implements IAgentPlugin { - readonly methods: WaciTypes = { - createOobQrCode: WaciQrCodeProvider.createOobQrCode.bind(this), - } - - /** {@inheritDoc WaciTypes.createOobQrCode} */ - private static async createOobQrCode(oobQRProps: OobQRProps, context: IRequiredContext): Promise { - const { onGenerate, bgColor, fgColor, level, size, title } = oobQRProps - - const payload = OutOfBandMessage.createPayload(oobQRProps) - const encoded = OutOfBandMessage.urlEncode(payload) - const url = oobQRProps.oobBaseUrl + encoded - - if (onGenerate) { - onGenerate(oobQRProps, payload) - } - await context.agent.emit(events.WACI_OOB_QR_CODE_CODE_CREATED, { props: oobQRProps, payload }) - - // @ts-ignore - return - } -} diff --git a/packages/waci-pex-qr-react/src/agent/qr-utils/outOfBandMessage.tsx b/packages/waci-pex-qr-react/src/agent/qr-utils/outOfBandMessage.tsx deleted file mode 100644 index f8bdbfb26..000000000 --- a/packages/waci-pex-qr-react/src/agent/qr-utils/outOfBandMessage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { OobPayload, OobQRProps } from '../../types/WaciTypes' -import base64url from 'base64url' - -export class OutOfBandMessage { - public static createPayload(oobQRProps: OobQRProps): OobPayload { - return { - type: oobQRProps.type, - id: oobQRProps.id, - from: oobQRProps.from, - body: { ...oobQRProps.body }, - } - } - - public static toJson(payload: OobPayload): string { - return JSON.stringify(payload).replace('goalCode', 'goal-code') - } - - public static urlEncode(payload: OobPayload) { - return base64url.encode(this.toJson(payload)) - } -} diff --git a/packages/waci-pex-qr-react/src/agent/utils/ReactQr.tsx b/packages/waci-pex-qr-react/src/agent/utils/ReactQr.tsx new file mode 100644 index 000000000..14879d5b3 --- /dev/null +++ b/packages/waci-pex-qr-react/src/agent/utils/ReactQr.tsx @@ -0,0 +1,58 @@ +import { CreateElementArgs, CreateValueArgs, events, IRequiredContext, QRType, ValueResult } from '../../types/IQRCodeGenerator' +import QRCode from 'react-qr-code' +import React from 'react' + +export async function generateQRCodeValue( + args: CreateValueArgs | CreateElementArgs, + context?: IRequiredContext +): Promise> { + return generateQRCodeValueImpl(args, args, context) +} + +export async function generateQRCodeValueImpl( + args: CreateValueArgs | CreateElementArgs, + orig: CreateValueArgs | CreateElementArgs, + context?: IRequiredContext +): Promise> { + const { onGenerate } = orig + const { id } = orig.data + + const value = args.data.object + + const result: ValueResult = { + id, + value, + data: orig.data, + renderingProps: 'renderingProps' in orig ? orig.renderingProps : undefined, + context, + } + + if (onGenerate) { + onGenerate(result) + } + if (context) { + context.agent.emit(events.QR_CODE_CODE_CREATED, result) + } + + return result +} + +export async function generateQRCodeReactElement( + args: CreateElementArgs, + context: IRequiredContext +): Promise { + return generateQRCodeReactElementImpl(args, args, context) +} + +export async function generateQRCodeReactElementImpl( + args: CreateElementArgs, + orig: CreateElementArgs, + context: IRequiredContext +): Promise { + const { renderingProps } = args + const { bgColor, fgColor, level, size, title } = renderingProps + const result: ValueResult = await generateQRCodeValueImpl(args, orig, context) + + // @ts-ignore + return +} diff --git a/packages/waci-pex-qr-react/src/agent/utils/didCommOutOfBandMessage.tsx b/packages/waci-pex-qr-react/src/agent/utils/didCommOutOfBandMessage.tsx new file mode 100644 index 000000000..1bf22b0fe --- /dev/null +++ b/packages/waci-pex-qr-react/src/agent/utils/didCommOutOfBandMessage.tsx @@ -0,0 +1,12 @@ +import { DIDCommV2OOBInvitation } from '../../types/IQRCodeGenerator' +import base64url from 'base64url' + +export class DidCommOutOfBandMessage { + public static toJson(props: DIDCommV2OOBInvitation): string { + return JSON.stringify(props).replace('goalCode', 'goal-code') + } + + public static urlEncode(payload: DIDCommV2OOBInvitation) { + return base64url(this.toJson(payload)) + } +} diff --git a/packages/waci-pex-qr-react/src/agent/utils/index.ts b/packages/waci-pex-qr-react/src/agent/utils/index.ts new file mode 100644 index 000000000..87baf193d --- /dev/null +++ b/packages/waci-pex-qr-react/src/agent/utils/index.ts @@ -0,0 +1,2 @@ +export { generateQRCodeReactElement, generateQRCodeValue } from './ReactQr' +export * from './didCommOutOfBandMessage' diff --git a/packages/waci-pex-qr-react/src/index.ts b/packages/waci-pex-qr-react/src/index.ts index e45936eb0..5a007e4fd 100644 --- a/packages/waci-pex-qr-react/src/index.ts +++ b/packages/waci-pex-qr-react/src/index.ts @@ -2,5 +2,5 @@ * @public */ -export * from './agent/WaciQrCodeProvider' -export * from './types/WaciTypes' +export * from './agent/QrCodeProvider' +export * from './types/IQRCodeGenerator' diff --git a/packages/waci-pex-qr-react/src/types/IQRCodeGenerator.ts b/packages/waci-pex-qr-react/src/types/IQRCodeGenerator.ts new file mode 100644 index 000000000..b5208b896 --- /dev/null +++ b/packages/waci-pex-qr-react/src/types/IQRCodeGenerator.ts @@ -0,0 +1,125 @@ +import { IAgentContext, IPluginMethodMap } from '@veramo/core' + +export interface IQRCodeGenerator extends IPluginMethodMap { + didCommOobInvitationElement( + args: CreateElementArgs, + context: IRequiredContext + ): Promise + + didCommOobInvitationValue( + args: CreateValueArgs, + context: IRequiredContext + ): Promise + + siopv2Element(args: CreateElementArgs, context: IRequiredContext): Promise + + siopv2Value(args: CreateValueArgs, context: IRequiredContext): Promise + + uriElement(args: CreateElementArgs, context: IRequiredContext): Promise +} + +export interface CreateValueArgs { + onGenerate?: (result: ValueResult) => void + data: QRData +} + +export interface CreateElementArgs extends CreateValueArgs { + renderingProps: QRRenderingProps +} + +export interface ValueResult { + id: string + value: string + data: QRData + renderingProps?: QRRenderingProps + context?: IRequiredContext +} + +export enum QRType { + // OIDC4VCI = 'openid-credential-offer', + + URI = 'uri', + SIOPV2 = 'openid-vc', + DIDCOMM_V2_OOB_INVITATION = 'https://didcomm.org/out-of-band/2.0/invitation', +} + +/* + +export interface QRContent { + state?: string + nonce?: string + qrValue?: string +} +*/ + +export type SIOPv2Scheme = 'openid' | 'openid-vc' | string +export interface SIOPv2DataWithScheme { + scheme?: SIOPv2Scheme + requestUri: string +} + +export interface DIDCommV2OOBInvitationData { + baseURI: string + oobInvitation: DIDCommV2OOBInvitation +} + +/** + * { + * "type": "https://didcomm.org/out-of-band/2.0/invitation", + * "id": "599f3638-b563-4937-9487-dfe55099d900", + * "from": "did:example:verifier", + * "body": { + * "goal_code": "streamlined-vp", + * "accept": ['didcomm/v2'] + * } + * } + */ +export interface DIDCommV2OOBInvitation { + type: 'https://didcomm.org/out-of-band/2.0/invitation' + id: string + from: DID + + body: Body +} + +export type URIData = string + +export type DID = string + +export interface Body { + goal_code: GoalCode + accept: [AcceptMode] +} + +export type GoalCode = 'streamlined-vp' | 'streamlined-vc' + +export type AcceptMode = 'didcomm/v2' | string + +/*OIDC4VP = 'oidc4vp', +SIOPV2_WITH_OIDC4VP = 'siopv2+oidc4vp', +SIOP_V2 = 'siopv2',*/ + +export enum StatusCode { + OK = 'OK', + CREATED = 'CREATED', +} + +export interface QRData { + id: string + type: T + object: D +} + +export interface QRRenderingProps { + bgColor?: string + fgColor?: string + level?: 'L' | 'M' | 'Q' | 'H' + size?: number + title?: string +} + +export enum events { + QR_CODE_CODE_CREATED = 'QrCodeCreated', +} + +export type IRequiredContext = IAgentContext> diff --git a/packages/waci-pex-qr-react/src/types/WaciTypes.ts b/packages/waci-pex-qr-react/src/types/WaciTypes.ts deleted file mode 100644 index 891993c6b..000000000 --- a/packages/waci-pex-qr-react/src/types/WaciTypes.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { IAgentContext, IPluginMethodMap } from '@veramo/core' - -export interface WaciTypes extends IPluginMethodMap { - createOobQrCode(oobQRProps: OobQRProps, context: IRequiredContext): Promise -} - -export enum QRType { - DID_AUTH_SIOP_V2 = 'siopv2', - DIDCOMM_V2_OOB = 'https://didcomm.org/out-of-band/2.0/invitation', -} - -export interface QRContent { - state?: string - nonce?: string - qrValue?: string -} - -export enum GoalCode { - STREAMLINED_VP = 'streamlined-vp', - STREAMLINED_VC = 'streamlined-vc', -} - -export enum AcceptMode { - OIDC4VP = 'oidc4vp', - SIOPV2_WITH_OIDC4VP = 'siopv2+oidc4vp', - SIOP_V2 = 'siopv2', - DIDCOMM_V2 = 'didcomm/v2', -} - -export enum StatusCode { - OK = 'OK', - CREATED = 'CREATED', -} - -/* -TODO: See whether we need this. Not in the spec currently -export interface WebRedirect { - status: StatusCode | string - redirectUrl: string -} -*/ - -export interface Body { - goalCode: GoalCode - accept: [AcceptMode] -} - -export interface OobQRProps { - oobBaseUrl: string - type: QRType - // webRedirect?: WebRedirect Not in spec for OOB, do we really need it? - body: Body - id: string - from: string - onGenerate?: (props: OobQRProps, payload: OobPayload) => void - bgColor?: string - fgColor?: string - level?: 'L' | 'M' | 'Q' | 'H' - size?: number - title?: string -} - -export interface OobPayload { - type: QRType - id: string - from: string - body: Body - // webRedirect?: WebRedirect Not in spec for OOB, do we really need it? -} - -export enum events { - WACI_OOB_QR_CODE_CODE_CREATED = 'WaciOobQrCodeCreated', -} - -export type IRequiredContext = IAgentContext>