From bf800998504f95f01a5af079c328ee129d4bd473 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 1 Mar 2024 19:38:56 -0500 Subject: [PATCH 1/4] Update README.md --- README.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c5185a4..bad56c9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ -# Example Isomorphic TS/JS Lib Template _(@digitalcredentials/isomorphic-lib-template)_ +# Learner Credential Wallet Mock _(@digitalcredentials/lcw-mock)_ -[![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/isomorphic-lib-template/main.yml?branch=main)](https://github.com/digitalcredentials/isomorphic-lib-template/actions?query=workflow%3A%22Node.js+CI%22) -[![NPM Version](https://img.shields.io/npm/v/@digitalcredentials/isomorphic-lib-template.svg)](https://npm.im/@digitalcredentials/isomorphic-lib-template) +[![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/lcw-mock/main.yml?branch=main)](https://github.com/digitalcredentials/lcw-mock/actions?query=workflow%3A%22Node.js+CI%22) +[![NPM Version](https://img.shields.io/npm/v/@digitalcredentials/lcw-mock.svg)](https://npm.im/@digitalcredentials/lcw-mock) -> A Typescript/Javascript isomorphic library template, for use in the browser, Node.js, and React Native. +> A Typescript/Javascript library that plays the part normally played by the [Learner Credential Wallet (LCW)](https://lcw.app) in the exchange between a wallet and an issuer like [DCC Exchange Coordinator](https://github.com/digitalcredentials/workflow-coordinator). + +> Given a deeplink that normally opens the LCW, the lcw-mock will parse the link, construct a DIDAuth containing the value of the 'challenge' parameter from the deeplink along with the holder's DID, sign the DIDAuth, and POST it to the value of the `vc_request_url` from the deeplink. +> +> For use in the browser, Node.js, and React Native. ## Table of Contents @@ -16,7 +20,7 @@ ## Background -TBD +Makes it easier to test credential issuance - specifically the DIDAuth exchange between the LCW and the issuer - without having to use the LCW on a real phone or an emulator. ## Security @@ -31,7 +35,7 @@ TBD To install via NPM: ``` -npm install @digitalcredentials/isomorphic-lib-template +npm install @digitalcredentials/lcw-mock ``` ### Development @@ -39,14 +43,56 @@ npm install @digitalcredentials/isomorphic-lib-template To install locally (for development): ``` -git clone https://github.com/digitalcredentials/isomorphic-lib-template.git -cd isomorphic-lib-template +git clone https://github.com/digitalcredentials/lcw-mock.git +cd lcw-mock npm install ``` ## Usage -TBD +There are two methods available: + + +#### mockLCW(deeplink, did) + +The `did` is optional. If not supplied a fake did (did:key:z6MkqJAUa299LgRoKiaHtFG6KWVK1iUddAM5oUHsGRto9hfz) will be used. + +1. Parses the deeplink, which should look similar to: + +``` https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&challenge=f78e1e08-a2df-46ca-bb0b-6b08e3036815&vc_request_url=http://issuer.dcconsortium.org/exchange/68546cc1-9220-42c1-b0a5-78d429ed289b/f78e1e08-a2df-46ca-bb0b-6b08e3036815``` + +2. Using the value of the `challenge` query parameter from the deeplink, and the supplied did (or a fake if not supplied) constructs a signed DIDAuth like so: + +``` +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "holder": "did:key:z6MkqJAUa299LgRoKiaHtFG6KWVK1iUddAM5oUHsGRto9hfz", + "proof": { + "type": "Ed25519Signature2020", + "created": "2024-02-23T21:27:59Z", + "verificationMethod": "did:key:z6MkqJAUa299LgRoKiaHtFG6KWVK1iUddAM5oUHsGRto9hfz#z6MkqJAUa299LgRoKiaHtFG6KWVK1iUddAM5oUHsGRto9hfz", + "proofPurpose": "authentication", + "challenge": "f78e1e08-a2df-46ca-bb0b-6b08e3036815", + "proofValue": "z5J8qBwNT3dC7kuqAkXFEDWgx1NeCcd2Rcn5dyNbhWsZdJQVsUgGDfd8ZdUo2S3maEucuJ2vqgCDJRB6fqSxhd6jM" + } + } +``` + +3. Posts the signed DIDAuth to the value of the `vc_request_url` query parameter from the deeplink. + +#### getDIDAuth(deeplink, did) + +The `did` is optional. If not supplied a fake did will be used. + +Same as `mockLCW` except it doesn't post the DIDAuth the vc_request_url, and instead simply returns it. + +This DIDAuth can then be separately posted to the issuer exchange endpoint (from the value of the vc_request_url query parameter). ## Contribute From 0d9b168bb73ad89b067a25de0e935e9b66bc9fba Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 1 Mar 2024 19:59:17 -0500 Subject: [PATCH 2/4] Update README.md --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bad56c9..18161d5 100644 --- a/README.md +++ b/README.md @@ -53,15 +53,13 @@ npm install There are two methods available: -#### mockLCW(deeplink, did) - -The `did` is optional. If not supplied a fake did (did:key:z6MkqJAUa299LgRoKiaHtFG6KWVK1iUddAM5oUHsGRto9hfz) will be used. +#### mockLCW(deeplink) 1. Parses the deeplink, which should look similar to: ``` https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&challenge=f78e1e08-a2df-46ca-bb0b-6b08e3036815&vc_request_url=http://issuer.dcconsortium.org/exchange/68546cc1-9220-42c1-b0a5-78d429ed289b/f78e1e08-a2df-46ca-bb0b-6b08e3036815``` -2. Using the value of the `challenge` query parameter from the deeplink, and the supplied did (or a fake if not supplied) constructs a signed DIDAuth like so: +2. Using the value of the `challenge` query parameter from the deeplink, and a holder DID (did:key:TODO add here and should match those below) constructs a signed DIDAuth like so: ``` { @@ -86,9 +84,7 @@ The `did` is optional. If not supplied a fake did (did:key:z6MkqJAUa299LgRoKiaHt 3. Posts the signed DIDAuth to the value of the `vc_request_url` query parameter from the deeplink. -#### getDIDAuth(deeplink, did) - -The `did` is optional. If not supplied a fake did will be used. +#### getDIDAuth(deeplink) Same as `mockLCW` except it doesn't post the DIDAuth the vc_request_url, and instead simply returns it. From b58d566af17b0689be1edac051aa85cd62a7f311 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Wed, 6 Mar 2024 08:57:08 -0500 Subject: [PATCH 3/4] first implementation --- .github/workflows/main.yml | 30 +++++++++++++------------- README.md | 2 +- package.json | 18 ++++++++++------ src/Example.ts | 8 ------- src/LCWMock.ts | 44 ++++++++++++++++++++++++++++++++++++++ src/declarations.d.ts | 3 ++- src/index.ts | 4 ++-- test/Example.spec.ts | 9 -------- test/LCWMock.spec.ts | 37 ++++++++++++++++++++++++++++++++ 9 files changed, 113 insertions(+), 42 deletions(-) delete mode 100644 src/Example.ts create mode 100644 src/LCWMock.ts delete mode 100644 test/Example.spec.ts create mode 100644 test/LCWMock.spec.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 20fcb80..2abb88c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,22 +20,22 @@ jobs: run: npm run test-node env: CI: true - test-karma: - runs-on: ubuntu-latest + # test-karma: + # runs-on: ubuntu-latest # needs: [lint] - timeout-minutes: 10 - strategy: - matrix: - node-version: [18.x] - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - name: Run karma tests - run: npm run test-karma + # timeout-minutes: 10 + # strategy: + # matrix: + # node-version: [18.x] + # steps: + # - uses: actions/checkout@v2 + # - name: Use Node.js ${{ matrix.node-version }} + # uses: actions/setup-node@v1 + # with: + # node-version: ${{ matrix.node-version }} + # - run: npm install + # - name: Run karma tests + # run: npm run test-karma lint: runs-on: ubuntu-latest strategy: diff --git a/README.md b/README.md index 18161d5..76a0b26 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ > Given a deeplink that normally opens the LCW, the lcw-mock will parse the link, construct a DIDAuth containing the value of the 'challenge' parameter from the deeplink along with the holder's DID, sign the DIDAuth, and POST it to the value of the `vc_request_url` from the deeplink. > -> For use in the browser, Node.js, and React Native. +> For use in the browser and Node.js. ## Table of Contents diff --git a/package.json b/package.json index c1d5bf0..193542d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@digitalcredentials/isomorphic-lib-template", - "description": "A Typescript/Javascript isomorphic library template, for use in the browser, Node.js, and React Native.", + "name": "@digitalcredentials/lcw-mock", + "description": "Mocks the part of the [Learner Credential Wallet (LCW)](https://lcw.app) in the exchange between a wallet and an issuer .", "version": "0.0.1", "scripts": { "build": "npm run clear && tsc -d && tsc -p tsconfig.esm.json", @@ -22,6 +22,11 @@ "browser": "dist/esm/index.js", "types": "dist/esm/index.d.ts", "dependencies": { + "@digitalcredentials/ed25519-signature-2020": "^4.0.0", + "@digitalcredentials/ed25519-verification-key-2020": "^4.0.0", + "@digitalcredentials/security-document-loader": "^5.0.0", + "@digitalcredentials/vc": "^7.0.0", + "axios": "^1.6.7" }, "resolutions": { "@typescript-eslint/typescript-estree": "^6.1.6" @@ -46,7 +51,8 @@ "rimraf": "^5.0.1", "ts-node": "^10.9.1", "ts-standard": "^12.0.2", - "typescript": "5.2.2" + "typescript": "5.2.2", + "axios-mock-adapter": "^1.22.0" }, "publishConfig": { "access": "public" @@ -76,8 +82,8 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/digitalcredentials/isomorphic-lib-template" + "url": "https://github.com/digitalcredentials/lcw-mock" }, - "homepage": "https://github.com/digitalcredentials/isomorphic-lib-template", - "bugs": "https://github.com/digitalcredentials/isomorphic-lib-template/issues" + "homepage": "https://github.com/digitalcredentials/lcw-mock", + "bugs": "https://github.com/digitalcredentials/lcw-mock/issues" } diff --git a/src/Example.ts b/src/Example.ts deleted file mode 100644 index c2a4980..0000000 --- a/src/Example.ts +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * Copyright (c) 2023 Digital Credentials Consortium. All rights reserved. - */ -export class Example { - public hello (): string { - return 'world' - } -} diff --git a/src/LCWMock.ts b/src/LCWMock.ts new file mode 100644 index 0000000..36b6693 --- /dev/null +++ b/src/LCWMock.ts @@ -0,0 +1,44 @@ +import axios from 'axios' +import { signPresentation, createPresentation } from '@digitalcredentials/vc' +import { Ed25519VerificationKey2020 } from '@digitalcredentials/ed25519-verification-key-2020' +import { Ed25519Signature2020 } from '@digitalcredentials/ed25519-signature-2020' +import { securityLoader } from '@digitalcredentials/security-document-loader' + +const holderDID = 'did:key:z6MkvL5yVCgPhYvQwSoSRQou6k6ZGfD5mNM57HKxufEXwfnP' + +const documentLoader = securityLoader().build() +let suite: any = null + +const getSuite = async (): Promise => { + if (suite === null) { + const key = await Ed25519VerificationKey2020.generate( + { + seed: new Uint8Array([ + 217, 87, 166, 30, 75, 106, 132, 55, + 32, 120, 171, 23, 116, 73, 254, 74, + 230, 16, 127, 91, 2, 252, 224, 96, + 184, 172, 245, 157, 58, 217, 91, 240 + ]), + controller: holderDID + } + ) + suite = new Ed25519Signature2020({ key }) + } + return suite +} + +export const getDIDAuth = async (challenge: string): Promise => { + const presentation = createPresentation({ holderDID }) + const suite = await getSuite() + const didAuth = await signPresentation({ presentation, suite, challenge, documentLoader }) + return didAuth +} + +export const mockLCW = async (deepLink: string): Promise => { + const parsedDeepLink = new URL(deepLink) + const vcRequestUrl = parsedDeepLink.searchParams.get('vc_request_url') as string // should be http://localhost:4004/exchange?challenge=VOclS8ZiMs&auth_type=bearer + const challenge = parsedDeepLink.searchParams.get('challenge') as string // the challenge that the exchange service generated + const didAuth = await getDIDAuth(challenge) + const response = await axios.post(vcRequestUrl, didAuth) + return response.data +} diff --git a/src/declarations.d.ts b/src/declarations.d.ts index d4dea51..ccf1f6f 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -1 +1,2 @@ -// declare module 'jsonld' +declare module '@digitalcredentials/ed25519-signature-2020' +declare module '@digitalcredentials/vc' diff --git a/src/index.ts b/src/index.ts index 9d5f922..2f2d58b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ /*! - * Copyright (c) 2023 Digital Credentials Consortium. All rights reserved. + * Copyright (c) 2024 Digital Credentials Consortium. All rights reserved. */ -export { Example } from './Example' +export * from './LCWMock' diff --git a/test/Example.spec.ts b/test/Example.spec.ts deleted file mode 100644 index 3bf5e63..0000000 --- a/test/Example.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { expect } from 'chai' -import { Example } from '../src' - -describe('Example', () => { - it('calls function', async () => { - const ex = new Example() - expect(ex.hello()).to.equal('world') - }) -}) diff --git a/test/LCWMock.spec.ts b/test/LCWMock.spec.ts new file mode 100644 index 0000000..35d4669 --- /dev/null +++ b/test/LCWMock.spec.ts @@ -0,0 +1,37 @@ +import { expect } from 'chai' +import axios from 'axios' +import MockAdapter from 'axios-mock-adapter' + +import { mockLCW, getDIDAuth } from '../src' + +const mock = new MockAdapter(axios) + +const challenge = 'f78e1e08-a2df-46ca-bb0b-6b08e3036815' +const vcRequestURL = 'https://issuer.dcconsortium.org/exchange/68546cc1-9220-42c1-b0a5-78d429ed289b/f78e1e08-a2df-46ca-bb0b-6b08e3036815' +const deeplink = `https://lcw.app/request.html?issuer=issuer.example.com&auth_type=bearer&challenge=${challenge}&vc_request_url=${vcRequestURL}` + +describe('LCWMock', () => { + describe('getDIDAuth', () => { + it('generates didAuth', async () => { + const didAuth = await getDIDAuth(challenge) + expect(didAuth.proof.proofValue.length).to.be.greaterThan(3) + expect(didAuth.proof.challenge).to.equal(challenge) + expect(didAuth.type).to.eql(['VerifiablePresentation']) + }) + }) + + describe('mockLCW', () => { + it.only('calls the deeplink with a DIDAuth', async () => { + mock + .onPost(vcRequestURL, { + asymmetricMatch: function (didAuth: any) { + return didAuth.proof.challenge === challenge && + didAuth.proof.proofValue != null + } + }) + .reply(200, { name: 'thisWouldBeTheReturnedVC' }) + const response = await mockLCW(deeplink) + expect(response.name).to.equal('thisWouldBeTheReturnedVC') + }) + }) +}) From 8f5e92bdca746e975683e9f0a8201b27c5efa840 Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Fri, 1 Aug 2025 15:01:42 -0400 Subject: [PATCH 4/4] update libs --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 193542d..c320a46 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,11 @@ "browser": "dist/esm/index.js", "types": "dist/esm/index.d.ts", "dependencies": { - "@digitalcredentials/ed25519-signature-2020": "^4.0.0", - "@digitalcredentials/ed25519-verification-key-2020": "^4.0.0", - "@digitalcredentials/security-document-loader": "^5.0.0", - "@digitalcredentials/vc": "^7.0.0", - "axios": "^1.6.7" + "@digitalcredentials/ed25519-signature-2020": "^7.0.0", + "@digitalcredentials/ed25519-verification-key-2020": "^5.0.0-beta.1", + "@digitalcredentials/security-document-loader": "^8.0.0", + "@digitalcredentials/vc": "^10.0.0", + "axios": "^1.6.7" }, "resolutions": { "@typescript-eslint/typescript-estree": "^6.1.6" @@ -37,6 +37,7 @@ "@types/node": "^20.4.6", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", + "axios-mock-adapter": "^1.22.0", "chai": "^4.3.7", "cross-env": "^7.0.3", "eslint": "^8.46.0", @@ -51,8 +52,7 @@ "rimraf": "^5.0.1", "ts-node": "^10.9.1", "ts-standard": "^12.0.2", - "typescript": "5.2.2", - "axios-mock-adapter": "^1.22.0" + "typescript": "5.2.2" }, "publishConfig": { "access": "public"