From 20c4d4c9f323b6b45060e2ba7f7668ebebbde0d4 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 24 Oct 2023 22:29:49 +0100 Subject: [PATCH 01/79] feat: add support for JSON-LD parsing --- package-lock.json | 136 ++++++++-- package.json | 11 +- src/common/common.mock.ts | 50 +++- src/common/common.test.ts | 341 ++++++++++++++++++++++++- src/common/common.ts | 110 +++++--- src/issue/issue.test.ts | 31 ++- src/issue/issue.ts | 15 +- src/parser/contexts/data-integrity.ts | 94 +++++++ src/parser/contexts/ed25519-2020.ts | 113 ++++++++ src/parser/contexts/index.ts | 43 ++++ src/parser/contexts/inrupt-vc.ts | 84 ++++++ src/parser/contexts/inrupt.ts | 149 +++++++++++ src/parser/contexts/revocation-list.ts | 68 +++++ src/parser/contexts/status-list.ts | 68 +++++ src/parser/contexts/vc.ts | 275 ++++++++++++++++++++ src/parser/jsonld.test.ts | 143 +++++++++++ src/parser/jsonld.ts | 129 ++++++++++ 17 files changed, 1778 insertions(+), 82 deletions(-) create mode 100644 src/parser/contexts/data-integrity.ts create mode 100644 src/parser/contexts/ed25519-2020.ts create mode 100644 src/parser/contexts/index.ts create mode 100644 src/parser/contexts/inrupt-vc.ts create mode 100644 src/parser/contexts/inrupt.ts create mode 100644 src/parser/contexts/revocation-list.ts create mode 100644 src/parser/contexts/status-list.ts create mode 100644 src/parser/contexts/vc.ts create mode 100644 src/parser/jsonld.test.ts create mode 100644 src/parser/jsonld.ts diff --git a/package-lock.json b/package-lock.json index 0cbfd370..1f9d387b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,13 @@ "license": "MIT", "dependencies": { "@inrupt/solid-client": "^1.25.2", - "@inrupt/universal-fetch": "^1.0.1" + "@inrupt/universal-fetch": "^1.0.1", + "content-type": "^1.0.5", + "event-emitter-promisify": "^1.1.0", + "jsonld-context-parser": "^2.3.3", + "jsonld-streaming-parser": "^3.2.1", + "jsonld-streaming-serializer": "^2.1.0", + "n3": "^1.17.0" }, "devDependencies": { "@inrupt/eslint-config-lib": "^2.0.0", @@ -21,8 +27,10 @@ "@rdfjs/dataset": "2.0.1", "@rdfjs/types": "^1.1.0", "@rushstack/eslint-patch": "^1.1.4", + "@types/content-type": "^1.1.5", "@types/dotenv-flow": "^3.1.1", "@types/jest": "^29.2.2", + "@types/n3": "^1.16.0", "@types/node": "^20.1.2", "@types/rdfjs__dataset": "2.0.5", "dotenv-flow": "^3.2.0", @@ -30,6 +38,7 @@ "jest": "^29.3.0", "jest-environment-jsdom": "^29.3.0", "prettier": "^3.0.2", + "rdf-isomorphic": "^1.3.1", "rollup": "^3.1.0", "rollup-plugin-typescript2": "^0.35.0", "ts-jest": "^29.0.3", @@ -922,15 +931,15 @@ } }, "node_modules/@inrupt/jest-jsdom-polyfills": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@inrupt/jest-jsdom-polyfills/-/jest-jsdom-polyfills-2.5.0.tgz", - "integrity": "sha512-1n5Ob3nsIOMTuQzkfC/Y3N/vgDwfh4LwaLTAiSr99GaUyJtunSK5ce0atc5k83LyAom6y4ZjrExw6IEXE9ebEg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@inrupt/jest-jsdom-polyfills/-/jest-jsdom-polyfills-2.4.1.tgz", + "integrity": "sha512-VgeSfZ5JVwnzKIlxs1wzULIp7phIMWQHhLxIYQBvjmFfrChYd9HrM3IEcMNNa5OL9JQJBxo6ESPtYLQKbqFRWQ==", "dev": true, "dependencies": { "@peculiar/webcrypto": "^1.4.0", "@web-std/blob": "^3.0.5", "@web-std/file": "^3.0.3", - "undici": "^5.26.3" + "undici": "^5.25.2" } }, "node_modules/@inrupt/oidc-client": { @@ -1616,12 +1625,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", - "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", + "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", "dev": true, "dependencies": { - "playwright": "1.39.0" + "playwright": "1.38.1" }, "bin": { "playwright": "cli.js" @@ -1828,6 +1837,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/content-type": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.5.tgz", + "integrity": "sha512-dgMN+syt1xb7Hk8LU6AODOfPlvz5z1CbXpPuJE5ZrX9STfBOIXF09pEB8N7a97WT9dbngt3ksDCm6GW6yMrxfQ==", + "dev": true + }, "node_modules/@types/dotenv-flow": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/dotenv-flow/-/dotenv-flow-3.3.1.tgz", @@ -1908,6 +1923,16 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/n3": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.16.0.tgz", + "integrity": "sha512-g/67NVSihmIoIZT3/J462NhJrmpCw+5WUkkKqpCE9YxNEWzBwKavGPP+RUmG6DIm5GrW4GPunuxLJ0Yn/GgNjQ==", + "dev": true, + "dependencies": { + "@rdfjs/types": "^1.1.0", + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.6.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", @@ -3098,7 +3123,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -4385,6 +4409,11 @@ "node": ">= 0.6" } }, + "node_modules/event-emitter-promisify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/event-emitter-promisify/-/event-emitter-promisify-1.1.0.tgz", + "integrity": "sha512-uyHG8gjwYGDlKoo0Txtx/u1HI1ubj0FK0rVqI4O0s1EymQm4iAEMbrS5B+XFlSaS8SZ3xzoKX+YHRZk8Nk/bXg==" + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -5165,6 +5194,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -6548,9 +6587,9 @@ } }, "node_modules/jsonld-context-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/jsonld-context-parser/-/jsonld-context-parser-2.3.0.tgz", - "integrity": "sha512-c6w2GE57O26eWFjcPX6k6G86ootsIfpuVwhZKjCll0bVoDGBxr1P4OuU+yvgfnh1GJhAGErolfC7W1BklLjWMg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jsonld-context-parser/-/jsonld-context-parser-2.3.3.tgz", + "integrity": "sha512-H+REInOx7XI2ciF8wJV31D20Bh+ofBmEjN2Tkds51vypqDJIiD341E5g+hYyrEInIKRnbW58TN/Ehz+ACT0l0w==", "dependencies": { "@types/http-link-header": "^1.0.1", "@types/node": "^18.0.0", @@ -6569,9 +6608,9 @@ "integrity": "sha512-xNbS75FxH6P4UXTPUJp/zNPq6/xsfdJKussCWNOnz4aULWIRwMgP1LgaB5RiBnMX1DPCYenuqGZfnIAx5mbFLA==" }, "node_modules/jsonld-streaming-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonld-streaming-parser/-/jsonld-streaming-parser-3.2.0.tgz", - "integrity": "sha512-lJR1SCT364PGpFrOQaY+ZQ7qDWqqiT3IMK+AvZ83fo0LvltFn8/UyXvIFc3RO7YcaEjLahAF0otCi8vOq21NtQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonld-streaming-parser/-/jsonld-streaming-parser-3.2.1.tgz", + "integrity": "sha512-MZCUrQe3pBO2pk2i3BpyW9Yn2oZoe2RCRpHZAJa88S6tRyxbe7XcjWfTKAZv35obDJDIREgot4723VhbClJELw==", "dependencies": { "@bergos/jsonparse": "^1.4.0", "@rdfjs/types": "*", @@ -6585,6 +6624,18 @@ "readable-stream": "^4.0.0" } }, + "node_modules/jsonld-streaming-serializer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jsonld-streaming-serializer/-/jsonld-streaming-serializer-2.1.0.tgz", + "integrity": "sha512-COHdLoeMTnrqHMoFhN3PoAwqnrKrpPC7/ACb0WbELYvt+HSOIFN3v4IJP7fOtLNQ4GeaeYkvbeWJ7Jo4EjxMDw==", + "dependencies": { + "@rdfjs/types": "*", + "@types/readable-stream": "^2.3.13", + "buffer": "^6.0.3", + "jsonld-context-parser": "^2.0.0", + "readable-stream": "^4.0.0" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -6800,6 +6851,12 @@ "node": ">=6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -7483,12 +7540,12 @@ } }, "node_modules/playwright": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", - "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", + "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", "dev": true, "dependencies": { - "playwright-core": "1.39.0" + "playwright-core": "1.38.1" }, "bin": { "playwright": "cli.js" @@ -7501,9 +7558,9 @@ } }, "node_modules/playwright-core": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", - "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", + "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -7898,6 +7955,18 @@ "@rdfjs/types": "*" } }, + "node_modules/rdf-isomorphic": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rdf-isomorphic/-/rdf-isomorphic-1.3.1.tgz", + "integrity": "sha512-6uIhsXTVp2AtO6f41PdnRV5xZsa0zVZQDTBdn0br+DZuFf5M/YD+T6m8hKDUnALI6nFL/IujTMLgEs20MlNidQ==", + "dev": true, + "dependencies": { + "@rdfjs/types": "*", + "hash.js": "^1.1.7", + "rdf-string": "^1.6.0", + "rdf-terms": "^1.7.0" + } + }, "node_modules/rdf-js": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/rdf-js/-/rdf-js-4.0.2.tgz", @@ -7906,6 +7975,27 @@ "@rdfjs/types": "*" } }, + "node_modules/rdf-string": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/rdf-string/-/rdf-string-1.6.3.tgz", + "integrity": "sha512-HIVwQ2gOqf+ObsCLSUAGFZMIl3rh9uGcRf1KbM85UDhKqP+hy6qj7Vz8FKt3GA54RiThqK3mNcr66dm1LP0+6g==", + "dev": true, + "dependencies": { + "@rdfjs/types": "*", + "rdf-data-factory": "^1.1.0" + } + }, + "node_modules/rdf-terms": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/rdf-terms/-/rdf-terms-1.11.0.tgz", + "integrity": "sha512-iKlVgnMopRKl9pHVNrQrax7PtZKRCT/uJIgYqvuw1VVQb88zDvurtDr1xp0rt7N9JtKtFwUXoIQoEsjyRo20qQ==", + "dev": true, + "dependencies": { + "@rdfjs/types": "*", + "rdf-data-factory": "^1.1.0", + "rdf-string": "^1.6.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", diff --git a/package.json b/package.json index 4c596ae4..7335ef1a 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,10 @@ "@rdfjs/dataset": "2.0.1", "@rdfjs/types": "^1.1.0", "@rushstack/eslint-patch": "^1.1.4", + "@types/content-type": "^1.1.5", "@types/dotenv-flow": "^3.1.1", "@types/jest": "^29.2.2", + "@types/n3": "^1.16.0", "@types/node": "^20.1.2", "@types/rdfjs__dataset": "2.0.5", "dotenv-flow": "^3.2.0", @@ -71,6 +73,7 @@ "jest": "^29.3.0", "jest-environment-jsdom": "^29.3.0", "prettier": "^3.0.2", + "rdf-isomorphic": "^1.3.1", "rollup": "^3.1.0", "rollup-plugin-typescript2": "^0.35.0", "ts-jest": "^29.0.3", @@ -84,7 +87,13 @@ }, "dependencies": { "@inrupt/solid-client": "^1.25.2", - "@inrupt/universal-fetch": "^1.0.1" + "@inrupt/universal-fetch": "^1.0.1", + "content-type": "^1.0.5", + "event-emitter-promisify": "^1.1.0", + "jsonld-context-parser": "^2.3.3", + "jsonld-streaming-parser": "^3.2.1", + "jsonld-streaming-serializer": "^2.1.0", + "n3": "^1.17.0" }, "publishConfig": { "access": "public" diff --git a/src/common/common.mock.ts b/src/common/common.mock.ts index 4befe6f9..0bac7662 100644 --- a/src/common/common.mock.ts +++ b/src/common/common.mock.ts @@ -45,9 +45,15 @@ export type CredentialClaims = VerifiableClaims & { }; export const defaultVerifiableClaims: VerifiableClaims = { - "@context": { ex: "https://example.org/ns/" }, - id: "ex:someCredentialInstance", - type: [...defaultCredentialTypes, "ex:spaceDogCertificate"], + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.inrupt.com/credentials/v1.jsonld", + ], + id: "https://example.org/ns/someCredentialInstance", + type: [ + ...defaultCredentialTypes, + "https://example.org/ns/spaceDogCertificate", + ], proofType: "Ed25519Signature2018", proofCreated: "2021-08-19T16:08:31Z", proofVerificationMethod: @@ -63,16 +69,19 @@ export const defaultCredentialClaims: CredentialClaims = { issuanceDate: "1960-08-19T16:08:31Z", subjectId: "https://some.webid.provider/strelka", subjectClaims: { - "ex:status": "https://example.org/ns/GoodDog", - "ex:passengerOf": "https://example.org/ns/Korabl-Sputnik2", + "https://example.org/ns/status": "https://example.org/ns/GoodDog", + "https://example.org/ns/passengerOf": + "https://example.org/ns/Korabl-Sputnik2", }, }; export const mockPartialCredential = ( claims?: Partial, + id?: string, ): Record => { return { - id: claims?.id, + "@context": claims?.["@context"], + id: id ?? claims?.id, type: claims?.type, issuer: claims?.issuer, issuanceDate: claims?.issuanceDate, @@ -90,14 +99,39 @@ export const mockPartialCredential = ( }; }; +export const mockPartialCredential2Proofs = ( + claims?: Partial, + id?: string, +): Record => { + return { + ...mockPartialCredential(claims, id), + proof: [ + mockPartialCredential(claims, id).proof, + mockPartialCredential(claims, id).proof, + ], + }; +}; + export const mockCredential = ( claims: CredentialClaims, ): VerifiableCredential => { return mockPartialCredential(claims) as VerifiableCredential; }; -export const mockDefaultCredential = (): VerifiableCredential => { - return mockPartialCredential(defaultCredentialClaims) as VerifiableCredential; +export const mockDefaultCredential = (id?: string): VerifiableCredential => { + return mockPartialCredential( + defaultCredentialClaims, + id, + ) as VerifiableCredential; +}; + +export const mockDefaultCredential2Proofs = ( + id?: string, +): VerifiableCredential => { + return mockPartialCredential2Proofs( + defaultCredentialClaims, + id, + ) as VerifiableCredential; }; export const mockPartialPresentation = ( diff --git a/src/common/common.test.ts b/src/common/common.test.ts index c1f79147..c438898f 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -18,16 +18,18 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // - import { jest, describe, it, expect } from "@jest/globals"; import { Response } from "@inrupt/universal-fetch"; import type * as UniversalFetch from "@inrupt/universal-fetch"; +import { isomorphic } from "rdf-isomorphic"; +import { DataFactory, Store } from "n3"; import type { VerifiableCredential } from "./common"; import { concatenateContexts, getVerifiableCredential, isVerifiableCredential, isVerifiablePresentation, + normalizeVc, } from "./common"; import { defaultCredentialClaims, @@ -36,7 +38,9 @@ import { mockDefaultPresentation, mockPartialPresentation, defaultVerifiableClaims, + mockDefaultCredential2Proofs, } from "./common.mock"; +import { jsonLdStringToStore, jsonLdToStore } from "../parser/jsonld"; jest.mock("@inrupt/universal-fetch", () => { const fetchModule = jest.requireActual( @@ -44,10 +48,19 @@ jest.mock("@inrupt/universal-fetch", () => { ) as typeof UniversalFetch; return { ...fetchModule, - fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(), + fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(() => { + throw new Error("Fetch should not be called"); + }), }; }); +describe("normalizeVc", () => { + it("returns the same object", () => { + const obj = {}; + expect(normalizeVc(obj)).toEqual(obj); + }); +}); + describe("isVerifiableCredential", () => { it("returns true if all the expected fields are present in the credential", () => { expect(isVerifiableCredential(mockDefaultCredential())).toBe(true); @@ -265,7 +278,9 @@ describe("getVerifiableCredential", () => { "@inrupt/universal-fetch", ) as jest.Mocked; mockedFetchModule.fetch.mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultCredential())), + new Response(JSON.stringify(mockDefaultCredential()), { + headers: new Headers([["content-type", "application/json"]]), + }), ); const redirectUrl = new URL("https://redirect.url"); @@ -286,7 +301,9 @@ describe("getVerifiableCredential", () => { const mockedFetch = jest .fn<(typeof UniversalFetch)["fetch"]>() .mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultCredential())), + new Response(JSON.stringify(mockDefaultCredential()), { + headers: new Headers([["content-type", "application/json"]]), + }), ); await getVerifiableCredential("https://some.vc", { @@ -335,16 +352,328 @@ describe("getVerifiableCredential", () => { ).rejects.toThrow(/https:\/\/some.vc.*Verifiable Credential/); }); - it("returns the fetched VC and the redirect URL", async () => { + it.skip("throws if the dereferenced data has an unsupported content type", async () => { const mockedFetch = jest .fn<(typeof UniversalFetch)["fetch"]>() .mockResolvedValueOnce( new Response(JSON.stringify(mockDefaultCredential())), ); + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow(/unsupported Content-Type/); + }); + + it("throws if the dereferenced data is empty", async () => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify({}), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential" + ); + }); + + it("throws if the vc is a blank node", async () => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + "@type": "https://www.w3.org/2018/credentials#VerifiableCredential", + }), + { + headers: new Headers([["content-type", "application/json"]]), + }, + ), + ); + + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential" + ); + }); + + it("throws if the vc has a type that is a literal", async () => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + "@context": "https://www.w3.org/2018/credentials/v1", + "@id": "http://example.org/my/vc", + "http://www.w3.org/1999/02/22-rdf-syntax-ns#type": [ + { + "@id": + "https://www.w3.org/2018/credentials#VerifiableCredential", + }, + "str", + ], + }), + { + headers: new Headers([["content-type", "application/json"]]), + }, + ), + ); + + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential" + ); + }); + + it("throws if the dereferenced data has 2 vcs", async () => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response( + JSON.stringify([ + mockDefaultCredential(), + mockDefaultCredential("http://example.org/mockVC2"), + ]), + { + headers: new Headers([["content-type", "application/json"]]), + }, + ), + ); + + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential" + ); + }); + + it("throws if the dereferenced data has 2 proofs", async () => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mockDefaultCredential2Proofs()), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential" + ); + }); + + it("throws if the date field is not a valid xsd:dateTime", async () => { + const mocked = mockDefaultCredential(); + mocked.issuanceDate = "http://example.org/not/a/date"; + + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential" + ); + }); + + it("throws if the date field is a string", async () => { + const mocked = mockDefaultCredential(); + // @ts-expect-error issuanceDate is required on the VC type + delete mocked.issuanceDate; + mocked["https://www.w3.org/2018/credentials#issuanceDate"] = + "http://example.org/not/a/date"; + + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential" + ); + }); + + it("throws if the date field is an IRI", async () => { + const mocked = mockDefaultCredential(); + // @ts-expect-error issuanceDate is required on the VC type + delete mocked.issuanceDate; + mocked["https://www.w3.org/2018/credentials#issuanceDate"] = { + "@id": "http://example.org/not/a/date", + }; + + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential", + ); + }); + + it("throws if the issuer is a string", async () => { + const mocked = mockDefaultCredential(); + // @ts-expect-error issuer is of type string on the VC type + mocked.issuer = { "@value": "my string" }; + + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential" + ); + }); + + it("should handle credential subjects with multiple objects", async () => { + const mocked = mockDefaultCredential(); + mocked.credentialSubject = { + ...mocked.credentialSubject, + "https://example.org/ns/passengerOf": [ + { "@id": "http://example.org/v1" }, + { "@id": "http://example.org/v2" }, + ], + "https://example.org/my/predicate/i": { + "https://example.org/my/predicate": "object", + }, + }; + + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + const vc = await getVerifiableCredential("https://some.vc", { fetch: mockedFetch, }); - expect(vc).toStrictEqual(mockDefaultCredential()); + + // Since we have dataset properties in vc it should match the result + // but won't equal + expect(vc).toMatchObject(mocked); + // However we DO NOT want these properties showing up when we stringify + // the VC + expect(JSON.parse(JSON.stringify(vc))).toEqual(mocked); + }); + + it("throws if there are 2 proof values", async () => { + const mocked = mockDefaultCredential(); + // @ts-expect-error proofValue is a string not string[] in VC type + mocked.proof.proofValue = [mocked.proof.proofValue, "abc"]; + + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential" + ); + }); + + it("returns the fetched VC and the redirect URL", async () => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mockDefaultCredential()), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + + const vc = await getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }); + + const res = await jsonLdStringToStore( + JSON.stringify(mockDefaultCredential()), + ); + expect(vc).toMatchObject( + Object.assign(mockDefaultCredential(), { + size: 13, + }), + ); + + const meaninglessQuad = DataFactory.quad( + DataFactory.namedNode("http://example.org/a"), + DataFactory.namedNode("http://example.org/b"), + DataFactory.namedNode("http://example.org/c"), + ); + + const issuerQuad = DataFactory.quad( + DataFactory.namedNode("https://example.org/ns/someCredentialInstance"), + DataFactory.namedNode("https://www.w3.org/2018/credentials#issuer"), + DataFactory.namedNode("https://some.vc.issuer/in-ussr"), + ); + + expect(isomorphic([...vc], [...res])).toBe(true); + expect(isomorphic([...vc.match()], [...res])).toBe(true); + expect(() => vc.add(meaninglessQuad)).toThrow("Cannot mutate this dataset"); + expect(() => vc.delete(meaninglessQuad)).toThrow( + "Cannot mutate this dataset", + ); + expect(vc.has(meaninglessQuad)).toBe(false); + expect(vc.has(issuerQuad)).toBe(true); + expect(vc.size).toBe(13); + expect( + vc.match( + DataFactory.namedNode("https://example.org/ns/someCredentialInstance"), + ).size, + ).toBe(6); }); }); diff --git a/src/common/common.ts b/src/common/common.ts index c8e49120..c98ad98b 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -26,11 +26,21 @@ import type { UrlString } from "@inrupt/solid-client"; import { getIri, + getJsonLdParser, getSolidDataset, getThingAll, - getJsonLdParser, } from "@inrupt/solid-client"; import { fetch as uniFetch } from "@inrupt/universal-fetch"; +import type { DatasetCore } from "@rdfjs/types"; +import contentTypeParser from "content-type"; +import { Util } from "jsonld-streaming-serializer"; +import type { BlankNode, Store, Term } from "n3"; +import { DataFactory as DF } from "n3"; +import type { JsonLdContextNormalized } from "jsonld-context-parser"; +import { context } from "../parser/contexts"; +import VcContext from "../parser/contexts/vc"; +import type { ParseOptions } from "../parser/jsonld"; +import { jsonLdToStore } from "../parser/jsonld"; export type Iri = string; /** @@ -239,7 +249,7 @@ export function concatenateContexts(...contexts: unknown[]): unknown { contexts.forEach((additionalContext) => { // Case when the context is an array of IRIs and/or inline contexts if (Array.isArray(additionalContext)) { - additionalContext.forEach((context) => result.add(context)); + additionalContext.forEach((contextEntry) => result.add(contextEntry)); } else if (additionalContext !== null && additionalContext !== undefined) { // Case when the context is a single remote URI or a single inline context result.add(additionalContext); @@ -396,6 +406,51 @@ export async function getVerifiableCredentialApiConfiguration( }; } +/** + * @hidden + */ +export async function verifiableCredentialToDataset(vc: VerifiableCredential, options?: ParseOptions): Promise { + let store: DatasetCore; + try { + store = await jsonLdToStore(vc, options); + } catch (e) { + throw new Error( + `Parsing the Verifiable Credential as JSON-LD failed: ${e}`, + ); + } + + return { + ...vc, + // Make this a DatasetCore without polluting the object with + // all of the properties present in the N3.Store + [Symbol.iterator]() { + return store[Symbol.iterator](); + }, + has(quad) { + return store.has(quad); + }, + match(subject, predicate, object, graph) { + // We need to cast to DatasetCore because the N3.Store + // type uses an internal type for Term rather than the @rdfjs/types Term + return store.match(subject, predicate, object, graph); + }, + add() { + throw new Error("Cannot mutate this dataset"); + }, + delete() { + throw new Error("Cannot mutate this dataset"); + }, + get size() { + return store.size; + }, + // For backwards compatibility the dataset properties + // SHOULD NOT be included when we JSON.stringify the object + toJSON() { + return vc; + } + } +} + /** * Dereference a VC URL, and verify that the resulting content is valid. * @@ -407,32 +462,29 @@ export async function getVerifiableCredentialApiConfiguration( */ export async function getVerifiableCredential( vcUrl: UrlString, - options?: Partial<{ - fetch: typeof fetch; - }>, -): Promise { + options?: ParseOptions, +): Promise { const authFetch = options?.fetch ?? uniFetch; - return authFetch(vcUrl as string) - .then(async (response) => { - if (!response.ok) { - throw new Error( - `Fetching the Verifiable Credential [${vcUrl}] failed: ${response.status} ${response.statusText}`, - ); - } - try { - return normalizeVc(await response.json()); - } catch (e) { - throw new Error( - `Parsing the Verifiable Credential [${vcUrl}] as JSON failed: ${e}`, - ); - } - }) - .then((vc) => { - if (!isVerifiableCredential(vc)) { - throw new Error( - `The value received from [${vcUrl}] is not a Verifiable Credential`, - ); - } - return vc; - }); + const response = await authFetch(vcUrl); + + if (!response.ok) { + throw new Error( + `Fetching the Verifiable Credential [${vcUrl}] failed: ${response.status} ${response.statusText}`, + ); + } + + let vc: unknown | VerifiableCredential; + try { + vc = normalizeVc(await response.json()); + } catch (e) { + throw new Error( + `Parsing the Verifiable Credential [${vcUrl}] as JSON failed: ${e}`, + ); + } + if (!isVerifiableCredential(vc)) { + throw new Error( + `The value received from [${vcUrl}] is not a Verifiable Credential`, + ); + } + return await verifiableCredentialToDataset(vc, options); } diff --git a/src/issue/issue.test.ts b/src/issue/issue.test.ts index 6229701b..fde761ec 100644 --- a/src/issue/issue.test.ts +++ b/src/issue/issue.test.ts @@ -113,16 +113,28 @@ describe("issueVerifiableCredential", () => { const mockedFetch = jest .fn<(typeof UniversalFetch)["fetch"]>() .mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultCredential()), { status: 201 }), + new Response(JSON.stringify(mockDefaultCredential()), { + status: 201, + headers: new Headers([["content-type", "application/ld+json"]]), + }), ); - await expect( - issueVerifiableCredential( - "https://some.endpoint", - { "@context": ["https://some.context"] }, - { "@context": ["https://some.context"] }, - { fetch: mockedFetch }, - ), - ).resolves.toEqual(mockDefaultCredential()); + + const vc = await issueVerifiableCredential( + "https://some.endpoint", + { "@context": ["https://some.context"] }, + { "@context": ["https://some.context"] }, + { fetch: mockedFetch }, + ) + + expect( + vc, + ).toMatchObject({ ...mockDefaultCredential(), size: 13 }); + + expect( + JSON.parse(JSON.stringify(vc)), + ).toEqual(mockDefaultCredential()); + + }); it("sends a request to the specified issuer", async () => { @@ -396,6 +408,7 @@ describe("issueVerifiableCredential", () => { const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(mockedVc), { status: 201, + headers: new Headers([["content-type", "application/json"]]), }), ); const resultVc = await issueVerifiableCredential( diff --git a/src/issue/issue.ts b/src/issue/issue.ts index c8a1fd13..f919e729 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -25,13 +25,15 @@ import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; +import type { DatasetCore } from "@rdfjs/types"; import type { Iri, JsonLd, VerifiableCredential } from "../common/common"; import { - isVerifiableCredential, concatenateContexts, defaultContext, defaultCredentialTypes, + isVerifiableCredential, normalizeVc, + verifiableCredentialToDataset, } from "../common/common"; type OptionsType = { @@ -47,7 +49,7 @@ async function internal_issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: OptionsType, -): Promise { +): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { internalOptions.fetch = fallbackFetch; @@ -101,9 +103,10 @@ async function internal_issueVerifiableCredential( `The VC issuing endpoint [${issuerEndpoint}] could not successfully issue a VC: ${response.status} ${response.statusText}`, ); } + const jsonData = normalizeVc(await response.json()); if (isVerifiableCredential(jsonData)) { - return jsonData; + return verifiableCredentialToDataset(jsonData); } throw new Error( `The VC issuing endpoint [${issuerEndpoint}] returned an unexpected object: ${JSON.stringify( @@ -134,7 +137,7 @@ export async function issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: OptionsType, -): Promise; +): Promise; /** * @deprecated Please remove the `subjectId` parameter */ @@ -144,7 +147,7 @@ export async function issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: OptionsType, -): Promise; +): Promise; // The signature of the implementation here is a bit confusing, but it avoid // breaking changes until we remove the `subjectId` completely from the API export async function issueVerifiableCredential( @@ -153,7 +156,7 @@ export async function issueVerifiableCredential( subjectOrCredentialClaims: JsonLd | undefined, credentialClaimsOrOptions?: JsonLd | OptionsType, options?: OptionsType, -): Promise { +): Promise { if (typeof subjectIdOrClaims === "string") { // The function has been called with the deprecated signature, and the // subjectOrCredentialClaims parameter should be ignored. diff --git a/src/parser/contexts/data-integrity.ts b/src/parser/contexts/data-integrity.ts new file mode 100644 index 00000000..c9898768 --- /dev/null +++ b/src/parser/contexts/data-integrity.ts @@ -0,0 +1,94 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +export default { + "@context": { + id: "@id", + type: "@type", + "@protected": true, + proof: { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph", + }, + DataIntegrityProof: { + "@id": "https://w3id.org/security#DataIntegrityProof", + "@context": { + "@protected": true, + id: "@id", + type: "@type", + challenge: "https://w3id.org/security#challenge", + created: { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + }, + domain: "https://w3id.org/security#domain", + expires: { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + }, + nonce: "https://w3id.org/security#nonce", + proofPurpose: { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + id: "@id", + type: "@type", + assertionMethod: { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set", + }, + authentication: { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set", + }, + capabilityInvocation: { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set", + }, + capabilityDelegation: { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set", + }, + keyAgreement: { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set", + }, + }, + }, + cryptosuite: "https://w3id.org/security#cryptosuite", + proofValue: { + "@id": "https://w3id.org/security#proofValue", + "@type": "https://w3id.org/security#multibase", + }, + verificationMethod: { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id", + }, + }, + }, + }, +}; diff --git a/src/parser/contexts/ed25519-2020.ts b/src/parser/contexts/ed25519-2020.ts new file mode 100644 index 00000000..b65eb596 --- /dev/null +++ b/src/parser/contexts/ed25519-2020.ts @@ -0,0 +1,113 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +export default { + "@context": { + id: "@id", + type: "@type", + "@protected": true, + proof: { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph", + }, + Ed25519VerificationKey2020: { + "@id": "https://w3id.org/security#Ed25519VerificationKey2020", + "@context": { + "@protected": true, + id: "@id", + type: "@type", + controller: { + "@id": "https://w3id.org/security#controller", + "@type": "@id", + }, + revoked: { + "@id": "https://w3id.org/security#revoked", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + }, + publicKeyMultibase: { + "@id": "https://w3id.org/security#publicKeyMultibase", + "@type": "https://w3id.org/security#multibase", + }, + }, + }, + Ed25519Signature2020: { + "@id": "https://w3id.org/security#Ed25519Signature2020", + "@context": { + "@protected": true, + id: "@id", + type: "@type", + challenge: "https://w3id.org/security#challenge", + created: { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + }, + domain: "https://w3id.org/security#domain", + expires: { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime", + }, + nonce: "https://w3id.org/security#nonce", + proofPurpose: { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + id: "@id", + type: "@type", + assertionMethod: { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set", + }, + authentication: { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set", + }, + capabilityInvocation: { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set", + }, + capabilityDelegation: { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set", + }, + keyAgreement: { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set", + }, + }, + }, + proofValue: { + "@id": "https://w3id.org/security#proofValue", + "@type": "https://w3id.org/security#multibase", + }, + verificationMethod: { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id", + }, + }, + }, + }, +}; diff --git a/src/parser/contexts/index.ts b/src/parser/contexts/index.ts new file mode 100644 index 00000000..384f8ded --- /dev/null +++ b/src/parser/contexts/index.ts @@ -0,0 +1,43 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +import VC from "./vc"; +import Inrupt from "./inrupt"; +import vc from "./inrupt-vc"; +import integrity from "./data-integrity"; +import ed25519 from "./ed25519-2020"; +import revocation from "./revocation-list"; +import statusList from "./status-list"; + +const contextDefinitions = { + "https://www.w3.org/2018/credentials/v1": VC, + "https://schema.inrupt.com/credentials/v1.jsonld": Inrupt, +} as const; + +export const cachedContexts = { + "https://vc.inrupt.com/credentials/v1": vc, + "https://w3id.org/security/data-integrity/v1": integrity, + "https://w3id.org/vc-revocation-list-2020/v1": revocation, + "https://w3id.org/vc/status-list/2021/v1": statusList, + "https://w3id.org/security/suites/ed25519-2020/v1": ed25519, +}; + +export const context = Object.keys(contextDefinitions); +export default contextDefinitions; diff --git a/src/parser/contexts/inrupt-vc.ts b/src/parser/contexts/inrupt-vc.ts new file mode 100644 index 00000000..56ed156c --- /dev/null +++ b/src/parser/contexts/inrupt-vc.ts @@ -0,0 +1,84 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +export default { + "@context": { + "@version": 1.1, + "@protected": true, + ldp: "http://www.w3.org/ns/ldp#", + acl: "http://www.w3.org/ns/auth/acl#", + gc: "https://w3id.org/GConsent#", + vc: "http://www.w3.org/ns/solid/vc#", + xsd: "http://www.w3.org/2001/XMLSchema#", + issuerService: { "@id": "vc:issuerService", "@type": "@id" }, + statusService: { "@id": "vc:statusService", "@type": "@id" }, + verifierService: { "@id": "vc:verifierService", "@type": "@id" }, + derivationService: { "@id": "vc:derivationService", "@type": "@id" }, + proofService: { "@id": "vc:proofService", "@type": "@id" }, + availabilityService: { "@id": "vc:availabilityService", "@type": "@id" }, + submissionService: { "@id": "vc:submissionService", "@type": "@id" }, + supportedSignatureTypes: { + "@id": "vc:supportedSignatureTypes", + "@type": "@id", + }, + include: { "@id": "vc:include", "@type": "@id" }, + SolidAccessGrant: "vc:SolidAccessGrant", + SolidAccessRequest: "vc:SolidAccessRequest", + ExpiredVerifiableCredential: "vc:ExpiredVerifiableCredential", + inbox: { "@id": "ldp:inbox", "@type": "@id" }, + Read: "acl:Read", + Write: "acl:Write", + Append: "acl:Append", + mode: { "@id": "acl:mode", "@type": "@vocab" }, + Consent: "gc:Consent", + ConsentStatusExpired: "gc:ConsentStatusExpired", + ConsentStatusExplicitlyGiven: "gc:ConsentStatusExplicitlyGiven", + ConsentStatusGivenByDelegation: "gc:ConsentStatusGivenByDelegation", + ConsentStatusImplicitlyGiven: "gc:ConsentStatusImplicitlyGiven", + ConsentStatusInvalidated: "gc:ConsentStatusInvalidated", + ConsentStatusNotGiven: "gc:ConsentStatusNotGiven", + ConsentStatusRefused: "gc:ConsentStatusRefused", + ConsentStatusRequested: "gc:ConsentStatusRequested", + ConsentStatusUnknown: "gc:ConsentStatusUnknown", + ConsentStatusWithdrawn: "gc:ConsentStatusWithdrawn", + forPersonalData: { "@id": "gc:forPersonalData", "@type": "@id" }, + forProcessing: { "@id": "gc:forProcessing", "@type": "@id" }, + forPurpose: { "@id": "gc:forPurpose", "@type": "@id" }, + hasConsent: { "@id": "gc:hasConsent", "@type": "@id" }, + hasContext: { "@id": "gc:hasContext", "@type": "@id" }, + hasStatus: { "@id": "gc:hasStatus", "@type": "@vocab" }, + inMedium: { "@id": "gc:inMedium", "@type": "@id" }, + isConsentForDataSubject: { + "@id": "gc:isConsentForDataSubject", + "@type": "@id", + }, + isProvidedTo: { "@id": "gc:isProvidedTo", "@type": "@id" }, + isProvidedToPerson: { "@id": "gc:isProvidedToPerson", "@type": "@id" }, + isProvidedToController: { + "@id": "gc:isProvidedToController", + "@type": "@id", + }, + providedConsent: { "@id": "gc:providedConsent", "@type": "@id" }, + inherit: { + "@id": "urn:uuid:71ab2f68-a68b-4452-b968-dd23e0570227", + "@type": "xsd:boolean", + }, + }, +}; diff --git a/src/parser/contexts/inrupt.ts b/src/parser/contexts/inrupt.ts new file mode 100644 index 00000000..51e2841f --- /dev/null +++ b/src/parser/contexts/inrupt.ts @@ -0,0 +1,149 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +/** + * The custom Inrupt context for verifiable credentials + * @see https://schema.inrupt.com/credentials/v1.jsonld + */ +export default { + "@context": { + "@version": 1.1, + "@protected": true, + ldp: "http://www.w3.org/ns/ldp#", + acl: "http://www.w3.org/ns/auth/acl#", + gc: "https://w3id.org/GConsent#", + vc: "http://www.w3.org/ns/solid/vc#", + xsd: "http://www.w3.org/2001/XMLSchema#", + issuerService: { + "@id": "vc:issuerService", + "@type": "@id", + }, + statusService: { + "@id": "vc:statusService", + "@type": "@id", + }, + verifierService: { + "@id": "vc:verifierService", + "@type": "@id", + }, + derivationService: { + "@id": "vc:derivationService", + "@type": "@id", + }, + proofService: { + "@id": "vc:proofService", + "@type": "@id", + }, + availabilityService: { + "@id": "vc:availabilityService", + "@type": "@id", + }, + submissionService: { + "@id": "vc:submissionService", + "@type": "@id", + }, + supportedSignatureTypes: { + "@id": "vc:supportedSignatureTypes", + "@type": "@id", + }, + include: { + "@id": "vc:include", + "@type": "@id", + }, + SolidAccessGrant: "vc:SolidAccessGrant", + SolidAccessRequest: "vc:SolidAccessRequest", + ExpiredVerifiableCredential: "vc:ExpiredVerifiableCredential", + inbox: { + "@id": "ldp:inbox", + "@type": "@id", + }, + Read: "acl:Read", + Write: "acl:Write", + Append: "acl:Append", + mode: { + "@id": "acl:mode", + "@type": "@vocab", + }, + Consent: "gc:Consent", + ConsentStatusExpired: "gc:ConsentStatusExpired", + ConsentStatusExplicitlyGiven: "gc:ConsentStatusExplicitlyGiven", + ConsentStatusGivenByDelegation: "gc:ConsentStatusGivenByDelegation", + ConsentStatusImplicitlyGiven: "gc:ConsentStatusImplicitlyGiven", + ConsentStatusInvalidated: "gc:ConsentStatusInvalidated", + ConsentStatusNotGiven: "gc:ConsentStatusNotGiven", + ConsentStatusRefused: "gc:ConsentStatusRefused", + ConsentStatusRequested: "gc:ConsentStatusRequested", + ConsentStatusUnknown: "gc:ConsentStatusUnknown", + ConsentStatusWithdrawn: "gc:ConsentStatusWithdrawn", + forPersonalData: { + "@id": "gc:forPersonalData", + "@type": "@id", + }, + forProcessing: { + "@id": "gc:forProcessing", + "@type": "@id", + }, + forPurpose: { + "@id": "gc:forPurpose", + "@type": "@id", + }, + hasConsent: { + "@id": "gc:hasConsent", + "@type": "@id", + }, + hasContext: { + "@id": "gc:hasContext", + "@type": "@id", + }, + hasStatus: { + "@id": "gc:hasStatus", + "@type": "@vocab", + }, + inMedium: { + "@id": "gc:inMedium", + "@type": "@id", + }, + isConsentForDataSubject: { + "@id": "gc:isConsentForDataSubject", + "@type": "@id", + }, + isProvidedTo: { + "@id": "gc:isProvidedTo", + "@type": "@id", + }, + isProvidedToPerson: { + "@id": "gc:isProvidedToPerson", + "@type": "@id", + }, + isProvidedToController: { + "@id": "gc:isProvidedToController", + "@type": "@id", + }, + providedConsent: { + "@id": "gc:providedConsent", + "@type": "@id", + }, + inherit: { + "@id": "urn:uuid:71ab2f68-a68b-4452-b968-dd23e0570227", + "@type": "xsd:boolean", + }, + }, +} as const; diff --git a/src/parser/contexts/revocation-list.ts b/src/parser/contexts/revocation-list.ts new file mode 100644 index 00000000..08a7861b --- /dev/null +++ b/src/parser/contexts/revocation-list.ts @@ -0,0 +1,68 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +export default { + "@context": { + "@protected": true, + RevocationList2020Credential: { + "@id": + "https://w3id.org/vc-revocation-list-2020#RevocationList2020Credential", + "@context": { + "@protected": true, + + id: "@id", + type: "@type", + + description: "http://schema.org/description", + name: "http://schema.org/name", + }, + }, + RevocationList2020: { + "@id": "https://w3id.org/vc-revocation-list-2020#RevocationList2020", + "@context": { + "@protected": true, + + id: "@id", + type: "@type", + + encodedList: "https://w3id.org/vc-revocation-list-2020#encodedList", + }, + }, + + RevocationList2020Status: { + "@id": + "https://w3id.org/vc-revocation-list-2020#RevocationList2020Status", + "@context": { + "@protected": true, + + id: "@id", + type: "@type", + + revocationListCredential: { + "@id": + "https://w3id.org/vc-revocation-list-2020#revocationListCredential", + "@type": "@id", + }, + revocationListIndex: + "https://w3id.org/vc-revocation-list-2020#revocationListIndex", + }, + }, + }, +}; diff --git a/src/parser/contexts/status-list.ts b/src/parser/contexts/status-list.ts new file mode 100644 index 00000000..24653e81 --- /dev/null +++ b/src/parser/contexts/status-list.ts @@ -0,0 +1,68 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +export default { + "@context": { + "@protected": true, + + StatusList2021Credential: { + "@id": "https://w3id.org/vc/status-list#StatusList2021Credential", + "@context": { + "@protected": true, + + id: "@id", + type: "@type", + + description: "http://schema.org/description", + name: "http://schema.org/name", + }, + }, + + StatusList2021: { + "@id": "https://w3id.org/vc/status-list#StatusList2021", + "@context": { + "@protected": true, + + id: "@id", + type: "@type", + + statusPurpose: "https://w3id.org/vc/status-list#statusPurpose", + encodedList: "https://w3id.org/vc/status-list#encodedList", + }, + }, + + StatusList2021Entry: { + "@id": "https://w3id.org/vc/status-list#StatusList2021Entry", + "@context": { + "@protected": true, + + id: "@id", + type: "@type", + + statusPurpose: "https://w3id.org/vc/status-list#statusPurpose", + statusListIndex: "https://w3id.org/vc/status-list#statusListIndex", + statusListCredential: { + "@id": "https://w3id.org/vc/status-list#statusListCredential", + "@type": "@id", + }, + }, + }, + }, +}; diff --git a/src/parser/contexts/vc.ts b/src/parser/contexts/vc.ts new file mode 100644 index 00000000..36f6b852 --- /dev/null +++ b/src/parser/contexts/vc.ts @@ -0,0 +1,275 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +/** + * The Verifiable Credentials context. + * @see https://www.w3.org/2018/credentials/v1 + */ +export default { + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + VerifiableCredential: { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + cred: "https://www.w3.org/2018/credentials#", + sec: "https://w3id.org/security#", + xsd: "http://www.w3.org/2001/XMLSchema#", + credentialSchema: { + "@id": "cred:credentialSchema", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + cred: "https://www.w3.org/2018/credentials#", + JsonSchemaValidator2018: "cred:JsonSchemaValidator2018", + }, + }, + credentialStatus: { "@id": "cred:credentialStatus", "@type": "@id" }, + credentialSubject: { "@id": "cred:credentialSubject", "@type": "@id" }, + evidence: { "@id": "cred:evidence", "@type": "@id" }, + expirationDate: { + "@id": "cred:expirationDate", + "@type": "xsd:dateTime", + }, + holder: { "@id": "cred:holder", "@type": "@id" }, + issued: { "@id": "cred:issued", "@type": "xsd:dateTime" }, + issuer: { "@id": "cred:issuer", "@type": "@id" }, + issuanceDate: { "@id": "cred:issuanceDate", "@type": "xsd:dateTime" }, + proof: { "@id": "sec:proof", "@type": "@id", "@container": "@graph" }, + refreshService: { + "@id": "cred:refreshService", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + cred: "https://www.w3.org/2018/credentials#", + ManualRefreshService2018: "cred:ManualRefreshService2018", + }, + }, + termsOfUse: { "@id": "cred:termsOfUse", "@type": "@id" }, + validFrom: { "@id": "cred:validFrom", "@type": "xsd:dateTime" }, + validUntil: { "@id": "cred:validUntil", "@type": "xsd:dateTime" }, + }, + }, + VerifiablePresentation: { + "@id": "https://www.w3.org/2018/credentials#VerifiablePresentation", + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + cred: "https://www.w3.org/2018/credentials#", + sec: "https://w3id.org/security#", + holder: { "@id": "cred:holder", "@type": "@id" }, + proof: { "@id": "sec:proof", "@type": "@id", "@container": "@graph" }, + verifiableCredential: { + "@id": "cred:verifiableCredential", + "@type": "@id", + "@container": "@graph", + }, + }, + }, + EcdsaSecp256k1Signature2019: { + "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + sec: "https://w3id.org/security#", + xsd: "http://www.w3.org/2001/XMLSchema#", + challenge: "sec:challenge", + created: { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime", + }, + domain: "sec:domain", + expires: { "@id": "sec:expiration", "@type": "xsd:dateTime" }, + jws: "sec:jws", + nonce: "sec:nonce", + proofPurpose: { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + sec: "https://w3id.org/security#", + assertionMethod: { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set", + }, + authentication: { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set", + }, + }, + }, + proofValue: "sec:proofValue", + verificationMethod: { "@id": "sec:verificationMethod", "@type": "@id" }, + }, + }, + EcdsaSecp256r1Signature2019: { + "@id": "https://w3id.org/security#EcdsaSecp256r1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + sec: "https://w3id.org/security#", + xsd: "http://www.w3.org/2001/XMLSchema#", + challenge: "sec:challenge", + created: { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime", + }, + domain: "sec:domain", + expires: { "@id": "sec:expiration", "@type": "xsd:dateTime" }, + jws: "sec:jws", + nonce: "sec:nonce", + proofPurpose: { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + sec: "https://w3id.org/security#", + assertionMethod: { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set", + }, + authentication: { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set", + }, + }, + }, + proofValue: "sec:proofValue", + verificationMethod: { "@id": "sec:verificationMethod", "@type": "@id" }, + }, + }, + Ed25519Signature2018: { + "@id": "https://w3id.org/security#Ed25519Signature2018", + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + sec: "https://w3id.org/security#", + xsd: "http://www.w3.org/2001/XMLSchema#", + challenge: "sec:challenge", + created: { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime", + }, + domain: "sec:domain", + expires: { "@id": "sec:expiration", "@type": "xsd:dateTime" }, + jws: "sec:jws", + nonce: "sec:nonce", + proofPurpose: { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + sec: "https://w3id.org/security#", + assertionMethod: { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set", + }, + authentication: { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set", + }, + }, + }, + proofValue: "sec:proofValue", + verificationMethod: { "@id": "sec:verificationMethod", "@type": "@id" }, + }, + }, + RsaSignature2018: { + "@id": "https://w3id.org/security#RsaSignature2018", + "@context": { + "@version": 1.1, + "@protected": true, + challenge: "sec:challenge", + created: { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime", + }, + domain: "sec:domain", + expires: { "@id": "sec:expiration", "@type": "xsd:dateTime" }, + jws: "sec:jws", + nonce: "sec:nonce", + proofPurpose: { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + id: "@id", + type: "@type", + sec: "https://w3id.org/security#", + assertionMethod: { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set", + }, + authentication: { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set", + }, + }, + }, + proofValue: "sec:proofValue", + verificationMethod: { "@id": "sec:verificationMethod", "@type": "@id" }, + }, + }, + proof: { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph", + }, + }, +} as const; diff --git a/src/parser/jsonld.test.ts b/src/parser/jsonld.test.ts new file mode 100644 index 00000000..443e4348 --- /dev/null +++ b/src/parser/jsonld.test.ts @@ -0,0 +1,143 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { jest, it, describe, expect, beforeAll } from "@jest/globals"; +import { Response } from "@inrupt/universal-fetch"; +import type * as UniversalFetch from "@inrupt/universal-fetch"; +import { DataFactory as DF } from "n3"; +import { isomorphic } from "rdf-isomorphic"; +import type { JsonLdContextNormalized } from "jsonld-context-parser"; +import { jsonLdToStore, getVcContext } from "./jsonld"; + +const fetcher: (typeof UniversalFetch)["fetch"] = async (url) => { + if (url !== "https://example.com/myContext") { + throw new Error("Unexpected URL"); + } + + return new Response( + JSON.stringify({ + "@context": { + Person: "http://xmlns.com/foaf/0.1/Person", + xsd: "http://www.w3.org/2001/XMLSchema#", + name: "http://xmlns.com/foaf/0.1/name", + nickname: "http://xmlns.com/foaf/0.1/nick", + affiliation: "http://schema.org/affiliation", + }, + }), + { + headers: new Headers([["content-type", "application/ld+json"]]), + }, + ); +}; + +jest.mock("@inrupt/universal-fetch", () => { + const fetchModule = jest.requireActual( + "@inrupt/universal-fetch", + ) as typeof UniversalFetch; + return { + ...fetchModule, + fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(), + }; +}); + +const data = { + "@context": "https://www.w3.org/2018/credentials/v1", + id: "https://some.example#credential", + type: ["VerifiableCredential"], + issuer: "https://some.example", +}; + +const dataExampleContext = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://example.com/myContext", + ], + id: "https://some.example#credential", + name: "Inrupt", +}; + +const dataWithPrefix = { + "@context": [ + { ex: "https://some.example#" }, + "https://www.w3.org/2018/credentials/v1", + ], + id: "ex:credential", + type: ["VerifiableCredential"], + issuer: "https://some.example", +}; + +const result = [ + DF.quad( + DF.namedNode("https://some.example#credential"), + DF.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + DF.namedNode("https://www.w3.org/2018/credentials#VerifiableCredential"), + ), + DF.quad( + DF.namedNode("https://some.example#credential"), + DF.namedNode("https://www.w3.org/2018/credentials#issuer"), + DF.namedNode("https://some.example"), + ), +]; + +describe("jsonLdToStore", () => { + it("converting valid POJO to store", async () => { + expect(isomorphic([...(await jsonLdToStore(data))], result)).toBe(true); + }); +}); + +describe("getVcContext", () => { + let context: JsonLdContextNormalized; + + beforeAll(async () => { + context = await getVcContext(); + }); + + it("should be able to compact and expand IRIs from the VC context", () => { + expect( + context.compactIri( + "https://www.w3.org/2018/credentials#VerifiableCredential", + true, + ), + ).toBe("VerifiableCredential"); + expect(context.expandTerm("VerifiableCredential", true)).toBe( + "https://www.w3.org/2018/credentials#VerifiableCredential", + ); + }); + + it("should be able to compact and expand IRIs from the Inrupt context", () => { + expect(context.compactIri("https://w3id.org/GConsent#Consent", true)).toBe( + "Consent", + ); + expect(context.expandTerm("Consent", true)).toBe( + "https://w3id.org/GConsent#Consent", + ); + }); + + it("should not compact and expand IRIs not in the VC or Inrupt context", () => { + expect( + context.compactIri( + "https://example.org/credentials#VerifiableCredential", + true, + ), + ).toBe("https://example.org/credentials#VerifiableCredential"); + expect(context.expandTerm("VC", true)).toBe("VC"); + }); +}); diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts new file mode 100644 index 00000000..fac3c584 --- /dev/null +++ b/src/parser/jsonld.ts @@ -0,0 +1,129 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +/* eslint-disable max-classes-per-file */ +import defaultFetch from "@inrupt/universal-fetch"; +import { promisifyEventEmitter } from "event-emitter-promisify"; +import type { + IJsonLdContext, + JsonLdContextNormalized, +} from "jsonld-context-parser"; +import { ContextParser, FetchDocumentLoader } from "jsonld-context-parser"; +import { JsonLdParser } from "jsonld-streaming-parser"; +import { Store } from "n3"; +import CONTEXTS, { cachedContexts } from "./contexts"; +import type { JsonLd } from "../common/common"; + +/** + * A JSON-LD document loader with the standard context for VCs pre-loaded + */ +class CachedFetchDocumentLoader extends FetchDocumentLoader { + private contexts: Record; + + constructor( + contexts?: Record, + private readonly allowContextFetching = true, + ...args: ConstructorParameters + ) { + super(...args); + this.contexts = { ...contexts, ...cachedContexts, ...CONTEXTS }; + } + + public async load(url: string): Promise { + if (Object.keys(this.contexts).includes(url)) { + return this.contexts[url as keyof typeof CONTEXTS]; + } + if (!this.allowContextFetching) { + throw new Error(`Unexpected context requested [${url}]`); + } + return super.load(url); + } +} + +/** + * Creates a context for use with the VC library + */ +export function getVcContext( + ...contexts: IJsonLdContext[] +): Promise { + const myParser = new ContextParser({ + documentLoader: new CachedFetchDocumentLoader(), + }); + return myParser.parse([ + ...Object.values(CONTEXTS).map((x) => x["@context"]), + ...contexts, + ]); +} + +export interface ParseOptions { + fetch?: typeof globalThis.fetch; + baseIRI?: string; + contexts?: Record; + allowContextFetching?: boolean; +} + +/** + * Our internal JsonLd Parser with a cached VC context + */ +export class CachedJsonLdParser extends JsonLdParser { + constructor(options?: ParseOptions) { + super({ + documentLoader: new CachedFetchDocumentLoader( + options?.contexts, + options?.allowContextFetching, + options?.fetch ?? defaultFetch, + ), + baseIRI: options?.baseIRI, + }); + } +} + +/** + * Gets an N3 store from a JSON-LD string + * @param response A JSON-LD string + * @param options An optional fetch function for dereferencing remote contexts + * @returns A store containing the Quads in the JSON-LD response + */ +export async function jsonLdStringToStore( + data: string, + options?: ParseOptions, +) { + try { + const parser = new CachedJsonLdParser(options); + const store = new Store(); + const storePromise = promisifyEventEmitter(store.import(parser), store); + parser.write(data); + parser.end(); + return await storePromise; + } catch (e) { + throw new Error(`Error parsing JSON-LD: [${e}].`); + } +} + +/** + * Gets an N3 store from a JSON-LD as an Object + * @param response JSON-LD as an Object + * @param options An optional fetch function for dereferencing remote contexts + * @returns A store containing the Quads in the JSON-LD response + */ +export function jsonLdToStore(data: JsonLd, options?: ParseOptions) { + return jsonLdStringToStore(JSON.stringify(data), options); +} From 508818962dcc799ad1462065d614fdbf4245daa3 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 24 Oct 2023 22:37:40 +0100 Subject: [PATCH 02/79] chore: fix lint errors --- src/common/common.test.ts | 32 +++++++++++++------------- src/common/common.ts | 22 +++++++++--------- src/issue/issue.test.ts | 22 +++++++----------- src/parser/jsonld.test.ts | 47 +++------------------------------------ src/parser/jsonld.ts | 3 +-- 5 files changed, 38 insertions(+), 88 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index c438898f..9cd261e1 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -18,11 +18,12 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import { jest, describe, it, expect } from "@jest/globals"; -import { Response } from "@inrupt/universal-fetch"; import type * as UniversalFetch from "@inrupt/universal-fetch"; +import { Response } from "@inrupt/universal-fetch"; +import { describe, expect, it, jest } from "@jest/globals"; +import { DataFactory } from "n3"; import { isomorphic } from "rdf-isomorphic"; -import { DataFactory, Store } from "n3"; +import { jsonLdStringToStore } from "../parser/jsonld"; import type { VerifiableCredential } from "./common"; import { concatenateContexts, @@ -33,14 +34,13 @@ import { } from "./common"; import { defaultCredentialClaims, - mockPartialCredential, + defaultVerifiableClaims, mockDefaultCredential, + mockDefaultCredential2Proofs, mockDefaultPresentation, + mockPartialCredential, mockPartialPresentation, - defaultVerifiableClaims, - mockDefaultCredential2Proofs, } from "./common.mock"; -import { jsonLdStringToStore, jsonLdToStore } from "../parser/jsonld"; jest.mock("@inrupt/universal-fetch", () => { const fetchModule = jest.requireActual( @@ -380,7 +380,7 @@ describe("getVerifiableCredential", () => { fetch: mockedFetch, }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential" + "The value received from [https://some.vc] is not a Verifiable Credential", ); }); @@ -403,7 +403,7 @@ describe("getVerifiableCredential", () => { fetch: mockedFetch, }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential" + "The value received from [https://some.vc] is not a Verifiable Credential", ); }); @@ -434,7 +434,7 @@ describe("getVerifiableCredential", () => { fetch: mockedFetch, }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential" + "The value received from [https://some.vc] is not a Verifiable Credential", ); }); @@ -458,7 +458,7 @@ describe("getVerifiableCredential", () => { fetch: mockedFetch, }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential" + "The value received from [https://some.vc] is not a Verifiable Credential", ); }); @@ -476,7 +476,7 @@ describe("getVerifiableCredential", () => { fetch: mockedFetch, }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential" + "The value received from [https://some.vc] is not a Verifiable Credential", ); }); @@ -497,7 +497,7 @@ describe("getVerifiableCredential", () => { fetch: mockedFetch, }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential" + "The value received from [https://some.vc] is not a Verifiable Credential", ); }); @@ -521,7 +521,7 @@ describe("getVerifiableCredential", () => { fetch: mockedFetch, }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential" + "The value received from [https://some.vc] is not a Verifiable Credential", ); }); @@ -568,7 +568,7 @@ describe("getVerifiableCredential", () => { fetch: mockedFetch, }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential" + "The value received from [https://some.vc] is not a Verifiable Credential", ); }); @@ -623,7 +623,7 @@ describe("getVerifiableCredential", () => { fetch: mockedFetch, }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential" + "The value received from [https://some.vc] is not a Verifiable Credential", ); }); diff --git a/src/common/common.ts b/src/common/common.ts index c98ad98b..2ee46b60 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -32,13 +32,6 @@ import { } from "@inrupt/solid-client"; import { fetch as uniFetch } from "@inrupt/universal-fetch"; import type { DatasetCore } from "@rdfjs/types"; -import contentTypeParser from "content-type"; -import { Util } from "jsonld-streaming-serializer"; -import type { BlankNode, Store, Term } from "n3"; -import { DataFactory as DF } from "n3"; -import type { JsonLdContextNormalized } from "jsonld-context-parser"; -import { context } from "../parser/contexts"; -import VcContext from "../parser/contexts/vc"; import type { ParseOptions } from "../parser/jsonld"; import { jsonLdToStore } from "../parser/jsonld"; @@ -409,7 +402,10 @@ export async function getVerifiableCredentialApiConfiguration( /** * @hidden */ -export async function verifiableCredentialToDataset(vc: VerifiableCredential, options?: ParseOptions): Promise { +export async function verifiableCredentialToDataset( + vc: VerifiableCredential, + options?: ParseOptions, +): Promise { let store: DatasetCore; try { store = await jsonLdToStore(vc, options); @@ -447,8 +443,8 @@ export async function verifiableCredentialToDataset(vc: VerifiableCredential, op // SHOULD NOT be included when we JSON.stringify the object toJSON() { return vc; - } - } + }, + }; } /** @@ -462,7 +458,9 @@ export async function verifiableCredentialToDataset(vc: VerifiableCredential, op */ export async function getVerifiableCredential( vcUrl: UrlString, - options?: ParseOptions, + options?: ParseOptions & { + fetch?: typeof fetch; + }, ): Promise { const authFetch = options?.fetch ?? uniFetch; const response = await authFetch(vcUrl); @@ -486,5 +484,5 @@ export async function getVerifiableCredential( `The value received from [${vcUrl}] is not a Verifiable Credential`, ); } - return await verifiableCredentialToDataset(vc, options); + return verifiableCredentialToDataset(vc, options); } diff --git a/src/issue/issue.test.ts b/src/issue/issue.test.ts index fde761ec..b72bb4d8 100644 --- a/src/issue/issue.test.ts +++ b/src/issue/issue.test.ts @@ -119,22 +119,16 @@ describe("issueVerifiableCredential", () => { }), ); - const vc = await issueVerifiableCredential( - "https://some.endpoint", - { "@context": ["https://some.context"] }, - { "@context": ["https://some.context"] }, - { fetch: mockedFetch }, - ) - - expect( - vc, - ).toMatchObject({ ...mockDefaultCredential(), size: 13 }); - - expect( - JSON.parse(JSON.stringify(vc)), - ).toEqual(mockDefaultCredential()); + const vc = await issueVerifiableCredential( + "https://some.endpoint", + { "@context": ["https://some.context"] }, + { "@context": ["https://some.context"] }, + { fetch: mockedFetch }, + ); + expect(vc).toMatchObject({ ...mockDefaultCredential(), size: 13 }); + expect(JSON.parse(JSON.stringify(vc))).toEqual(mockDefaultCredential()); }); it("sends a request to the specified issuer", async () => { diff --git a/src/parser/jsonld.test.ts b/src/parser/jsonld.test.ts index 443e4348..7a3b8499 100644 --- a/src/parser/jsonld.test.ts +++ b/src/parser/jsonld.test.ts @@ -19,34 +19,12 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import { jest, it, describe, expect, beforeAll } from "@jest/globals"; -import { Response } from "@inrupt/universal-fetch"; import type * as UniversalFetch from "@inrupt/universal-fetch"; +import { beforeAll, describe, expect, it, jest } from "@jest/globals"; +import type { JsonLdContextNormalized } from "jsonld-context-parser"; import { DataFactory as DF } from "n3"; import { isomorphic } from "rdf-isomorphic"; -import type { JsonLdContextNormalized } from "jsonld-context-parser"; -import { jsonLdToStore, getVcContext } from "./jsonld"; - -const fetcher: (typeof UniversalFetch)["fetch"] = async (url) => { - if (url !== "https://example.com/myContext") { - throw new Error("Unexpected URL"); - } - - return new Response( - JSON.stringify({ - "@context": { - Person: "http://xmlns.com/foaf/0.1/Person", - xsd: "http://www.w3.org/2001/XMLSchema#", - name: "http://xmlns.com/foaf/0.1/name", - nickname: "http://xmlns.com/foaf/0.1/nick", - affiliation: "http://schema.org/affiliation", - }, - }), - { - headers: new Headers([["content-type", "application/ld+json"]]), - }, - ); -}; +import { getVcContext, jsonLdToStore } from "./jsonld"; jest.mock("@inrupt/universal-fetch", () => { const fetchModule = jest.requireActual( @@ -65,25 +43,6 @@ const data = { issuer: "https://some.example", }; -const dataExampleContext = { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://example.com/myContext", - ], - id: "https://some.example#credential", - name: "Inrupt", -}; - -const dataWithPrefix = { - "@context": [ - { ex: "https://some.example#" }, - "https://www.w3.org/2018/credentials/v1", - ], - id: "ex:credential", - type: ["VerifiableCredential"], - issuer: "https://some.example", -}; - const result = [ DF.quad( DF.namedNode("https://some.example#credential"), diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index fac3c584..c1aabf54 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -74,7 +74,6 @@ export function getVcContext( } export interface ParseOptions { - fetch?: typeof globalThis.fetch; baseIRI?: string; contexts?: Record; allowContextFetching?: boolean; @@ -89,7 +88,7 @@ export class CachedJsonLdParser extends JsonLdParser { documentLoader: new CachedFetchDocumentLoader( options?.contexts, options?.allowContextFetching, - options?.fetch ?? defaultFetch, + defaultFetch, ), baseIRI: options?.baseIRI, }); From 9af788150e6b45b737af3949e3b9d3bfb3d868e7 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 24 Oct 2023 23:39:49 +0100 Subject: [PATCH 03/79] chore: refactor verify functionality --- src/issue/issue.ts | 3 ++- src/lookup/derive.test.ts | 29 +++++++++++++++++++---------- src/lookup/derive.ts | 3 ++- src/lookup/query.test.ts | 15 ++++++++------- src/lookup/query.ts | 30 ++++++++++++++++++++++++------ src/verify/verify.test.ts | 17 ++++++++++++++--- src/verify/verify.ts | 37 ++++++++++--------------------------- 7 files changed, 79 insertions(+), 55 deletions(-) diff --git a/src/issue/issue.ts b/src/issue/issue.ts index f919e729..4aa258c2 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -35,6 +35,7 @@ import { normalizeVc, verifiableCredentialToDataset, } from "../common/common"; +import type { ParseOptions } from "../parser/jsonld"; type OptionsType = { fetch?: typeof fallbackFetch; @@ -48,7 +49,7 @@ async function internal_issueVerifiableCredential( issuerEndpoint: Iri, subjectClaims: JsonLd, credentialClaims?: JsonLd, - options?: OptionsType, + options?: OptionsType & ParseOptions, ): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { diff --git a/src/lookup/derive.test.ts b/src/lookup/derive.test.ts index e0ce7701..3e8222d8 100644 --- a/src/lookup/derive.test.ts +++ b/src/lookup/derive.test.ts @@ -27,6 +27,7 @@ import defaultGetVerifilableCredentialAllFromShape, { getVerifiableCredentialAllFromShape, } from "./derive"; import type * as QueryModule from "./query"; +import type { VerifiableCredential } from "../common/common"; jest.mock("@inrupt/universal-fetch", () => { const fetchModule = jest.requireActual( @@ -137,16 +138,24 @@ describe("getVerifiableCredentialAllFromShape", () => { const mockedFetch = jest .fn<(typeof UniversalFetch)["fetch"]>() .mockResolvedValue(mockDeriveEndpointDefaultResponse()); - await expect( - getVerifiableCredentialAllFromShape( - "https://some.endpoint", - { - "@context": ["https://some.context"], - credentialSubject: { id: "https://some.subject/" }, - }, - { fetch: mockedFetch }, - ), - ).resolves.toEqual(mockDefaultPresentation().verifiableCredential); + + const vc = await getVerifiableCredentialAllFromShape( + "https://some.endpoint", + { + "@context": ["https://some.context"], + credentialSubject: { id: "https://some.subject/" }, + }, + { fetch: mockedFetch }, + ); + + expect(vc).toMatchObject( + mockDefaultPresentation() + .verifiableCredential as VerifiableCredential[], + ); + + expect(JSON.parse(JSON.stringify(vc))).toEqual( + mockDefaultPresentation().verifiableCredential, + ); }); it("returns an empty array if the VP contains no VCs", async () => { diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index 78ea973a..f35af94a 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -20,6 +20,7 @@ // import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; +import type { DatasetCore } from "@rdfjs/types"; import type { Iri, VerifiableCredential } from "../common/common"; import { concatenateContexts, defaultContext } from "../common/common"; import type { VerifiablePresentationRequest } from "./query"; @@ -100,7 +101,7 @@ export async function getVerifiableCredentialAllFromShape( fetch: typeof fallbackFetch; includeExpiredVc: boolean; }>, -): Promise { +): Promise<(VerifiableCredential & DatasetCore)[]> { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { internalOptions.fetch = fallbackFetch; diff --git a/src/lookup/query.test.ts b/src/lookup/query.test.ts index 536802e0..fbcf6b8e 100644 --- a/src/lookup/query.test.ts +++ b/src/lookup/query.test.ts @@ -165,13 +165,14 @@ describe("query", () => { status: 200, }), ); - await expect( - query( - "https://example.org/query", - { query: [mockRequest] }, - { fetch: mockedFetch }, - ), - ).resolves.toStrictEqual(mockDefaultPresentation()); + + const vp = await query( + "https://example.org/query", + { query: [mockRequest] }, + { fetch: mockedFetch }, + ); + expect(vp).toMatchObject(mockDefaultPresentation()); + expect(JSON.parse(JSON.stringify(vp))).toEqual(mockDefaultPresentation()); }); it("normalizes the VP sent by the endpoint", async () => { diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 7d225be1..cc86b795 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -20,12 +20,18 @@ // import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; +import type { DatasetCore } from "@rdfjs/types"; import type { Iri, VerifiableCredential, VerifiablePresentation, } from "../common/common"; -import { isVerifiablePresentation, normalizeVp } from "../common/common"; +import { + isVerifiablePresentation, + normalizeVp, + verifiableCredentialToDataset, +} from "../common/common"; +import type { ParseOptions } from "../parser/jsonld"; /** * Based on https://w3c-ccg.github.io/vp-request-spec/#query-by-example. @@ -61,6 +67,10 @@ export type VerifiablePresentationRequest = { domain?: string; }; +interface ParsedVerifiablePresentation extends VerifiablePresentation { + verifiableCredential?: (VerifiableCredential & DatasetCore)[]; +} + /** * Send a Verifiable Presentation Request to a query endpoint in order to retrieve * all Verifiable Credentials matching the query, wrapped in a single Presentation. @@ -96,10 +106,11 @@ export type VerifiablePresentationRequest = { export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, - options?: Partial<{ - fetch: typeof fallbackFetch; - }>, -): Promise { + options?: ParseOptions & + Partial<{ + fetch: typeof fallbackFetch; + }>, +): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { internalOptions.fetch = fallbackFetch; @@ -132,5 +143,12 @@ export async function query( )}`, ); } - return data; + if (data.verifiableCredential) { + data.verifiableCredential = await Promise.all( + data.verifiableCredential.map((vc) => + verifiableCredentialToDataset(vc, options), + ), + ); + } + return data as ParsedVerifiablePresentation; } diff --git a/src/verify/verify.test.ts b/src/verify/verify.test.ts index 0e14e447..fbb22047 100644 --- a/src/verify/verify.test.ts +++ b/src/verify/verify.test.ts @@ -24,6 +24,7 @@ import { mocked } from "jest-mock"; import { Response } from "@inrupt/universal-fetch"; import type * as UniversalFetch from "@inrupt/universal-fetch"; import { + getVerifiableCredential, getVerifiableCredentialApiConfiguration, isVerifiableCredential, isVerifiablePresentation, @@ -179,7 +180,13 @@ describe("isValidVc", () => { verificationEndpoint: MOCK_VERIFY_ENDPOINT, }); - expect(mockedFetch).toHaveBeenCalledWith("https://example.com/someVc"); + expect(getVerifiableCredential).toHaveBeenCalledWith( + "https://example.com/someVc", + { + fetch: mockedFetch, + verificationEndpoint: "https://consent.example.com", + }, + ); }); it("throws if looking up the passed url fails", async () => { @@ -193,7 +200,9 @@ describe("isValidVc", () => { fetch: mockedFetch as typeof fetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, }), - ).rejects.toThrow(/example.com\/someVc.*400 Failed/); + ).rejects.toThrow( + "The request to [https://example.com/someVc] returned an unexpected response: undefined", + ); }); it("throws if looking up the passed url doesn't resolve to JSON", async () => { @@ -205,7 +214,9 @@ describe("isValidVc", () => { fetch: mockedFetch as typeof fetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, }), - ).rejects.toThrow(/Parsing.*example.com\/someVc/); + ).rejects.toThrow( + "The request to [https://example.com/someVc] returned an unexpected response: undefined", + ); }); it("throws if the passed url returns a non-vc", async () => { diff --git a/src/verify/verify.ts b/src/verify/verify.ts index 57b87f71..0fb17342 100644 --- a/src/verify/verify.ts +++ b/src/verify/verify.ts @@ -31,40 +31,22 @@ import type { VerifiablePresentation, } from "../common/common"; import { + getVerifiableCredential, getVerifiableCredentialApiConfiguration, isVerifiableCredential, isVerifiablePresentation, - normalizeVc, } from "../common/common"; +import type { ParseOptions } from "../parser/jsonld"; async function dereferenceVc( vc: VerifiableCredential | URL | UrlString, - fetcher: typeof fallbackFetch, + options?: ParseOptions, ): Promise { // This test passes for both URL and UrlString if (!vc.toString().startsWith("http")) { return vc as VerifiableCredential; } - // vc is either an IRI-shaped string or a URL object. In both - // cases, vc.toString() is an IRI. - const vcResponse = await fetcher(vc.toString()); - if (!vcResponse.ok) { - throw new Error( - `Dereferencing [${vc.toString()}] failed: ${vcResponse.status} ${ - vcResponse.statusText - }`, - ); - } - try { - return normalizeVc(await vcResponse.json()); - } catch (e) { - throw new Error( - `Parsing the value obtained when dereferencing [${vc.toString()}] as JSON failed: ${ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (e as any).toString() - }`, - ); - } + return getVerifiableCredential(vc.toString(), options); } /** @@ -87,14 +69,15 @@ async function dereferenceVc( */ export async function isValidVc( vc: VerifiableCredential | URL | UrlString, - options: Partial<{ + options?: Partial<{ fetch?: typeof fetch; verificationEndpoint?: UrlString; - }> = {}, + }> & + ParseOptions, ): Promise<{ checks: string[]; warnings: string[]; errors: string[] }> { - const fetcher = options.fetch ?? fallbackFetch; + const fetcher = options?.fetch ?? fallbackFetch; - const vcObject = await dereferenceVc(vc, fetcher); + const vcObject = await dereferenceVc(vc, options); if (!isVerifiableCredential(vcObject)) { throw new Error( @@ -108,7 +91,7 @@ export async function isValidVc( // Discover the consent endpoint from the resource part of the Access Grant. const verifierEndpoint = - options.verificationEndpoint ?? + options?.verificationEndpoint ?? (await getVerifiableCredentialApiConfiguration(vcObject.issuer)) .verifierService; From 382ba418379e8339fdf516202290368ced46e643 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 00:10:32 +0100 Subject: [PATCH 04/79] chore: get full test coverage --- src/common/common.test.ts | 55 +++++++++++++++++++++++++++++++++++++-- src/parser/jsonld.ts | 22 +++++++--------- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 9cd261e1..98ec20e3 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -19,8 +19,8 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import type * as UniversalFetch from "@inrupt/universal-fetch"; -import { Response } from "@inrupt/universal-fetch"; -import { describe, expect, it, jest } from "@jest/globals"; +import { Response, fetch as uniFetch } from "@inrupt/universal-fetch"; +import { describe, expect, it, jest, beforeEach } from "@jest/globals"; import { DataFactory } from "n3"; import { isomorphic } from "rdf-isomorphic"; import { jsonLdStringToStore } from "../parser/jsonld"; @@ -676,4 +676,55 @@ describe("getVerifiableCredential", () => { ).size, ).toBe(6); }); + + describe("with non cached contexts", () => { + const mockCredential = { + ...mockDefaultCredential(), + "@context": [ + ...(mockDefaultCredential()["@context"] as string[]), + "http://example.org/my/sample/context", + ], + }; + let mockedFetch: jest.Mock<(typeof UniversalFetch)["fetch"]>; + + beforeEach(() => { + mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mockCredential), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + }); + + it("errors if the context contains an IRI that is not cached", async () => { + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }), + ).rejects.toThrow( + "Unexpected context requested [http://example.org/my/sample/context]", + ); + }); + + it("errors if the context contains an IRI that is not cached or fetchable when allowedContextFetching", async () => { + (uniFetch as jest.Mock).mockResolvedValueOnce( + new Response( + JSON.stringify({ + "@context": {}, + }), + { + headers: new Headers([["content-type", "application/ld+json"]]), + }, + ), + ); + + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + allowContextFetching: true, + }), + ).resolves.toMatchObject(mockCredential); + }); + }); }); diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index c1aabf54..766fc69d 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -20,7 +20,7 @@ // /* eslint-disable max-classes-per-file */ -import defaultFetch from "@inrupt/universal-fetch"; +import { fetch as defaultFetch } from "@inrupt/universal-fetch"; import { promisifyEventEmitter } from "event-emitter-promisify"; import type { IJsonLdContext, @@ -40,10 +40,10 @@ class CachedFetchDocumentLoader extends FetchDocumentLoader { constructor( contexts?: Record, - private readonly allowContextFetching = true, + private readonly allowContextFetching = false, ...args: ConstructorParameters ) { - super(...args); + super(args[0] ?? defaultFetch); this.contexts = { ...contexts, ...cachedContexts, ...CONTEXTS }; } @@ -105,16 +105,12 @@ export async function jsonLdStringToStore( data: string, options?: ParseOptions, ) { - try { - const parser = new CachedJsonLdParser(options); - const store = new Store(); - const storePromise = promisifyEventEmitter(store.import(parser), store); - parser.write(data); - parser.end(); - return await storePromise; - } catch (e) { - throw new Error(`Error parsing JSON-LD: [${e}].`); - } + const parser = new CachedJsonLdParser(options); + const store = new Store(); + const storePromise = promisifyEventEmitter(store.import(parser), store); + parser.write(data); + parser.end(); + return storePromise; } /** From 1b948c53720730104247f29e59924a84bfe4c34d Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 00:12:49 +0100 Subject: [PATCH 05/79] chore: rebuild lockfile from main --- package-lock.json | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1f9d387b..0cb6c551 100644 --- a/package-lock.json +++ b/package-lock.json @@ -931,15 +931,15 @@ } }, "node_modules/@inrupt/jest-jsdom-polyfills": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@inrupt/jest-jsdom-polyfills/-/jest-jsdom-polyfills-2.4.1.tgz", - "integrity": "sha512-VgeSfZ5JVwnzKIlxs1wzULIp7phIMWQHhLxIYQBvjmFfrChYd9HrM3IEcMNNa5OL9JQJBxo6ESPtYLQKbqFRWQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inrupt/jest-jsdom-polyfills/-/jest-jsdom-polyfills-2.5.0.tgz", + "integrity": "sha512-1n5Ob3nsIOMTuQzkfC/Y3N/vgDwfh4LwaLTAiSr99GaUyJtunSK5ce0atc5k83LyAom6y4ZjrExw6IEXE9ebEg==", "dev": true, "dependencies": { "@peculiar/webcrypto": "^1.4.0", "@web-std/blob": "^3.0.5", "@web-std/file": "^3.0.3", - "undici": "^5.25.2" + "undici": "^5.26.3" } }, "node_modules/@inrupt/oidc-client": { @@ -1625,12 +1625,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", - "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", "dev": true, "dependencies": { - "playwright": "1.38.1" + "playwright": "1.39.0" }, "bin": { "playwright": "cli.js" @@ -1838,9 +1838,9 @@ } }, "node_modules/@types/content-type": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.5.tgz", - "integrity": "sha512-dgMN+syt1xb7Hk8LU6AODOfPlvz5z1CbXpPuJE5ZrX9STfBOIXF09pEB8N7a97WT9dbngt3ksDCm6GW6yMrxfQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.7.tgz", + "integrity": "sha512-dSM/IQ1fgM1aSQ2PlHR4uWQGDjs9SY+/ilm228CIs9hwlyIfW+q5asThulBforWR6ktt/o8L8m6GPW/Fz1dk2A==", "dev": true }, "node_modules/@types/dotenv-flow": { @@ -1924,9 +1924,9 @@ "dev": true }, "node_modules/@types/n3": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.16.0.tgz", - "integrity": "sha512-g/67NVSihmIoIZT3/J462NhJrmpCw+5WUkkKqpCE9YxNEWzBwKavGPP+RUmG6DIm5GrW4GPunuxLJ0Yn/GgNjQ==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.16.3.tgz", + "integrity": "sha512-6PDn2/tUtveGQONiFJDOpl2Aa/YnmwQhdQ8SznUJVFmjI/NEBBdNhnkmYeEFmvOQbhbIeGR+SfmTk71TdqJ5mg==", "dev": true, "dependencies": { "@rdfjs/types": "^1.1.0", @@ -7540,12 +7540,12 @@ } }, "node_modules/playwright": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", - "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", "dev": true, "dependencies": { - "playwright-core": "1.38.1" + "playwright-core": "1.39.0" }, "bin": { "playwright": "cli.js" @@ -7558,9 +7558,9 @@ } }, "node_modules/playwright-core": { - "version": "1.38.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", - "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", "dev": true, "bin": { "playwright-core": "cli.js" From 1c8b26a5a9be101e8e5e5d5948c240e3a4093785 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 00:37:25 +0100 Subject: [PATCH 06/79] chore: globally cache the common CachedFetchDocumentLoader configuration --- src/common/common.test.ts | 16 +++++++++++++++- src/parser/jsonld.ts | 21 ++++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 98ec20e3..051fd26c 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -707,7 +707,7 @@ describe("getVerifiableCredential", () => { ); }); - it("errors if the context contains an IRI that is not cached or fetchable when allowedContextFetching", async () => { + it("resolves if allowContextFetching is enabled and the context can be fetched", async () => { (uniFetch as jest.Mock).mockResolvedValueOnce( new Response( JSON.stringify({ @@ -726,5 +726,19 @@ describe("getVerifiableCredential", () => { }), ).resolves.toMatchObject(mockCredential); }); + + it("resolves if the context is cached", async () => { + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + allowContextFetching: true, + contexts: { + "http://example.org/my/sample/context": { + "@context": {}, + }, + }, + }), + ).resolves.toMatchObject(mockCredential); + }); }); }); diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index 766fc69d..fbef6a9b 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -79,17 +79,32 @@ export interface ParseOptions { allowContextFetching?: boolean; } +let reusableDocumentLoader: CachedFetchDocumentLoader; + /** * Our internal JsonLd Parser with a cached VC context */ export class CachedJsonLdParser extends JsonLdParser { constructor(options?: ParseOptions) { - super({ - documentLoader: new CachedFetchDocumentLoader( + let documentLoader: CachedFetchDocumentLoader; + + if (!options?.contexts && !options?.allowContextFetching) { + reusableDocumentLoader ??= new CachedFetchDocumentLoader( options?.contexts, options?.allowContextFetching, defaultFetch, - ), + ); + documentLoader = reusableDocumentLoader; + } else { + documentLoader = new CachedFetchDocumentLoader( + options.contexts, + options.allowContextFetching, + defaultFetch, + ); + } + + super({ + documentLoader, baseIRI: options?.baseIRI, }); } From e6ea8f045921e7c1b9e05b78650b6c9cdfce3791 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 00:47:48 +0100 Subject: [PATCH 07/79] chore: temporarily disable getVerifiableCredentialAllFromShape test --- e2e/node/e2e.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index 8dc6f2b4..7f7b20f8 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -147,7 +147,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { }); describe("lookup VCs", () => { - it("returns all VC issued matching a given shape", async () => { + it.skip("returns all VC issued matching a given shape", async () => { const result = await getVerifiableCredentialAllFromShape( new URL("derive", env.vcProvider).href, { From 4117faae308713f38d52e1c32852df93169e1b6e Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:52:08 +0100 Subject: [PATCH 08/79] chore: test if sequentially parsing vcs resolves memory issue --- e2e/node/e2e.test.ts | 2 +- src/lookup/query.ts | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index 7f7b20f8..8dc6f2b4 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -147,7 +147,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { }); describe("lookup VCs", () => { - it.skip("returns all VC issued matching a given shape", async () => { + it("returns all VC issued matching a given shape", async () => { const result = await getVerifiableCredentialAllFromShape( new URL("derive", env.vcProvider).href, { diff --git a/src/lookup/query.ts b/src/lookup/query.ts index cc86b795..f6437b51 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -144,11 +144,16 @@ export async function query( ); } if (data.verifiableCredential) { - data.verifiableCredential = await Promise.all( - data.verifiableCredential.map((vc) => - verifiableCredentialToDataset(vc, options), - ), - ); + const newVerifiableCredential: (VerifiableCredential & DatasetCore)[] = [] + for (const vc of data.verifiableCredential) { + newVerifiableCredential.push(await verifiableCredentialToDataset(vc, options)); + } + data.verifiableCredential = newVerifiableCredential; + // data.verifiableCredential = await Promise.all( + // data.verifiableCredential.map((vc) => + // verifiableCredentialToDataset(vc, options), + // ), + // ); } return data as ParsedVerifiablePresentation; } From a5297369ed25a400e2966eabf1ddf6c76ba64b17 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:11:26 +0100 Subject: [PATCH 09/79] chore: benchmark parsing --- src/lookup/query.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lookup/query.ts b/src/lookup/query.ts index f6437b51..cbef4876 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -143,10 +143,16 @@ export async function query( )}`, ); } + console.log("querying", data.verifiableCredential?.length, "vcs"); if (data.verifiableCredential) { - const newVerifiableCredential: (VerifiableCredential & DatasetCore)[] = [] + const newVerifiableCredential: (VerifiableCredential & DatasetCore)[] = []; for (const vc of data.verifiableCredential) { - newVerifiableCredential.push(await verifiableCredentialToDataset(vc, options)); + console.time(vc.id); + newVerifiableCredential.push( + // eslint-disable-next-line no-await-in-loop + await verifiableCredentialToDataset(vc, options), + ); + console.timeEnd(vc.id); } data.verifiableCredential = newVerifiableCredential; // data.verifiableCredential = await Promise.all( From 15516043ee0333053d5f8b8c2a764023e6101e86 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:54:01 +0100 Subject: [PATCH 10/79] perf: cache the normalized context --- src/parser/jsonld.ts | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index fbef6a9b..f90a4c81 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -24,6 +24,9 @@ import { fetch as defaultFetch } from "@inrupt/universal-fetch"; import { promisifyEventEmitter } from "event-emitter-promisify"; import type { IJsonLdContext, + IJsonLdContextNormalizedRaw, + IParseOptions, + JsonLdContext, JsonLdContextNormalized, } from "jsonld-context-parser"; import { ContextParser, FetchDocumentLoader } from "jsonld-context-parser"; @@ -31,6 +34,7 @@ import { JsonLdParser } from "jsonld-streaming-parser"; import { Store } from "n3"; import CONTEXTS, { cachedContexts } from "./contexts"; import type { JsonLd } from "../common/common"; +import { ParsingContext } from "jsonld-streaming-parser/lib/ParsingContext"; /** * A JSON-LD document loader with the standard context for VCs pre-loaded @@ -79,7 +83,32 @@ export interface ParseOptions { allowContextFetching?: boolean; } -let reusableDocumentLoader: CachedFetchDocumentLoader; +const reusableDocumentLoader = new CachedFetchDocumentLoader(); + +class MyContextParser extends ContextParser { + private cachedParsing: Record> = {}; + + async parse(context: JsonLdContext, options?: IParseOptions): Promise { + if ( + typeof options?.baseIRI === 'undefined' + && options?.processingMode === 1.1 + && Object.keys(options?.parentContext ?? {}).length === 0 + && Array.isArray(context) + && context.every(c => typeof c === 'string') + ) { + const str = JSON.stringify(context); + return this.cachedParsing[str] ??= super.parse(context, options); + } + + return super.parse(context); + } + + load(url: string) { + return super.load(url); + } +} + +const reusableContextParser = new MyContextParser({ documentLoader: reusableDocumentLoader }) /** * Our internal JsonLd Parser with a cached VC context @@ -89,11 +118,6 @@ export class CachedJsonLdParser extends JsonLdParser { let documentLoader: CachedFetchDocumentLoader; if (!options?.contexts && !options?.allowContextFetching) { - reusableDocumentLoader ??= new CachedFetchDocumentLoader( - options?.contexts, - options?.allowContextFetching, - defaultFetch, - ); documentLoader = reusableDocumentLoader; } else { documentLoader = new CachedFetchDocumentLoader( @@ -107,6 +131,11 @@ export class CachedJsonLdParser extends JsonLdParser { documentLoader, baseIRI: options?.baseIRI, }); + + if (!options?.contexts && !options?.allowContextFetching) { + // @ts-ignore + this.parsingContext.contextParser = reusableContextParser; + } } } From ded8f1f01319ffc066953de841dae145b33ae6a3 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 12:06:28 +0100 Subject: [PATCH 11/79] chore: re-supply parse options --- src/parser/jsonld.ts | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index f90a4c81..11257118 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -32,9 +32,9 @@ import type { import { ContextParser, FetchDocumentLoader } from "jsonld-context-parser"; import { JsonLdParser } from "jsonld-streaming-parser"; import { Store } from "n3"; +import { ParsingContext } from "jsonld-streaming-parser/lib/ParsingContext"; import CONTEXTS, { cachedContexts } from "./contexts"; import type { JsonLd } from "../common/common"; -import { ParsingContext } from "jsonld-streaming-parser/lib/ParsingContext"; /** * A JSON-LD document loader with the standard context for VCs pre-loaded @@ -88,19 +88,24 @@ const reusableDocumentLoader = new CachedFetchDocumentLoader(); class MyContextParser extends ContextParser { private cachedParsing: Record> = {}; - async parse(context: JsonLdContext, options?: IParseOptions): Promise { + async parse( + context: JsonLdContext, + options?: IParseOptions, + ): Promise { if ( - typeof options?.baseIRI === 'undefined' - && options?.processingMode === 1.1 - && Object.keys(options?.parentContext ?? {}).length === 0 - && Array.isArray(context) - && context.every(c => typeof c === 'string') - ) { + typeof options?.baseIRI === "undefined" && + options?.processingMode === 1.1 && + Object.keys(options?.parentContext ?? {}).length === 0 && + Array.isArray(context) && + context.every((c) => typeof c === "string") + ) { const str = JSON.stringify(context); - return this.cachedParsing[str] ??= super.parse(context, options); + console.log('cache hit', str in this.cachedParsing); + return (this.cachedParsing[str] ??= super.parse(context, options)); + // return super.parse(context, options) } - return super.parse(context); + return super.parse(context, options); } load(url: string) { @@ -108,7 +113,9 @@ class MyContextParser extends ContextParser { } } -const reusableContextParser = new MyContextParser({ documentLoader: reusableDocumentLoader }) +const reusableContextParser = new MyContextParser({ + documentLoader: reusableDocumentLoader, +}); /** * Our internal JsonLd Parser with a cached VC context From 72ce3f80b460c13bc6b450cac2500ff47f1b0d6c Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 12:24:34 +0100 Subject: [PATCH 12/79] chore: cache child contexts --- src/parser/jsonld.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index 11257118..124a8ac2 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -88,6 +88,11 @@ const reusableDocumentLoader = new CachedFetchDocumentLoader(); class MyContextParser extends ContextParser { private cachedParsing: Record> = {}; + private parentContexts = new Map< + IJsonLdContextNormalizedRaw | undefined, + Map> + >(); + async parse( context: JsonLdContext, options?: IParseOptions, @@ -100,16 +105,25 @@ class MyContextParser extends ContextParser { context.every((c) => typeof c === "string") ) { const str = JSON.stringify(context); - console.log('cache hit', str in this.cachedParsing); + console.log("cache hit", str in this.cachedParsing); return (this.cachedParsing[str] ??= super.parse(context, options)); // return super.parse(context, options) } + if (!Array.isArray(context)) { + if (!this.parentContexts.has(options?.parentContext)) { + this.parentContexts.set(options?.parentContext, new Map()); + } - return super.parse(context, options); - } + const childContext = this.parentContexts.get(options?.parentContext)!; - load(url: string) { - return super.load(url); + if (!childContext.has(context)) { + childContext.set(context, super.parse(context, options)); + } + + return childContext.get(context)!; + } + + return super.parse(context, options); } } From 974c442d4b1d276a04a76f42b3f405c4c2f42026 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 12:47:29 +0100 Subject: [PATCH 13/79] chore: fix lint errors and remove console.logs --- src/lookup/query.ts | 22 +--- src/parser/contexts/index.ts | 3 + src/parser/contexts/odrl.ts | 222 +++++++++++++++++++++++++++++++++++ src/parser/jsonld.ts | 19 ++- 4 files changed, 240 insertions(+), 26 deletions(-) create mode 100644 src/parser/contexts/odrl.ts diff --git a/src/lookup/query.ts b/src/lookup/query.ts index cbef4876..4ebd63be 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -143,23 +143,13 @@ export async function query( )}`, ); } - console.log("querying", data.verifiableCredential?.length, "vcs"); + if (data.verifiableCredential) { - const newVerifiableCredential: (VerifiableCredential & DatasetCore)[] = []; - for (const vc of data.verifiableCredential) { - console.time(vc.id); - newVerifiableCredential.push( - // eslint-disable-next-line no-await-in-loop - await verifiableCredentialToDataset(vc, options), - ); - console.timeEnd(vc.id); - } - data.verifiableCredential = newVerifiableCredential; - // data.verifiableCredential = await Promise.all( - // data.verifiableCredential.map((vc) => - // verifiableCredentialToDataset(vc, options), - // ), - // ); + data.verifiableCredential = await Promise.all( + data.verifiableCredential.map((vc) => + verifiableCredentialToDataset(vc, options), + ), + ); } return data as ParsedVerifiablePresentation; } diff --git a/src/parser/contexts/index.ts b/src/parser/contexts/index.ts index 384f8ded..afe51c16 100644 --- a/src/parser/contexts/index.ts +++ b/src/parser/contexts/index.ts @@ -25,6 +25,7 @@ import integrity from "./data-integrity"; import ed25519 from "./ed25519-2020"; import revocation from "./revocation-list"; import statusList from "./status-list"; +import odrl from "./odrl"; const contextDefinitions = { "https://www.w3.org/2018/credentials/v1": VC, @@ -37,6 +38,8 @@ export const cachedContexts = { "https://w3id.org/vc-revocation-list-2020/v1": revocation, "https://w3id.org/vc/status-list/2021/v1": statusList, "https://w3id.org/security/suites/ed25519-2020/v1": ed25519, + // FIXME: Double check if we need this + "http://www.w3.org/ns/odrl.jsonld": odrl, }; export const context = Object.keys(contextDefinitions); diff --git a/src/parser/contexts/odrl.ts b/src/parser/contexts/odrl.ts new file mode 100644 index 00000000..a236c00a --- /dev/null +++ b/src/parser/contexts/odrl.ts @@ -0,0 +1,222 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +export default { + "@context": { + odrl: "http://www.w3.org/ns/odrl/2/", + rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + rdfs: "http://www.w3.org/2000/01/rdf-schema#", + owl: "http://www.w3.org/2002/07/owl#", + skos: "http://www.w3.org/2004/02/skos/core#", + dct: "http://purl.org/dc/terms/", + xsd: "http://www.w3.org/2001/XMLSchema#", + vcard: "http://www.w3.org/2006/vcard/ns#", + foaf: "http://xmlns.com/foaf/0.1/", + schema: "http://schema.org/", + cc: "http://creativecommons.org/ns#", + + uid: "@id", + type: "@type", + + Policy: "odrl:Policy", + Rule: "odrl:Rule", + profile: { "@type": "@id", "@id": "odrl:profile" }, + + inheritFrom: { "@type": "@id", "@id": "odrl:inheritFrom" }, + + ConflictTerm: "odrl:ConflictTerm", + conflict: { "@type": "@vocab", "@id": "odrl:conflict" }, + perm: "odrl:perm", + prohibit: "odrl:prohibit", + invalid: "odrl:invalid", + + Agreement: "odrl:Agreement", + Assertion: "odrl:Assertion", + Offer: "odrl:Offer", + Privacy: "odrl:Privacy", + Request: "odrl:Request", + Set: "odrl:Set", + Ticket: "odrl:Ticket", + + Asset: "odrl:Asset", + AssetCollection: "odrl:AssetCollection", + relation: { "@type": "@id", "@id": "odrl:relation" }, + hasPolicy: { "@type": "@id", "@id": "odrl:hasPolicy" }, + + target: { "@type": "@id", "@id": "odrl:target" }, + output: { "@type": "@id", "@id": "odrl:output" }, + + partOf: { "@type": "@id", "@id": "odrl:partOf" }, + source: { "@type": "@id", "@id": "odrl:source" }, + + Party: "odrl:Party", + PartyCollection: "odrl:PartyCollection", + function: { "@type": "@vocab", "@id": "odrl:function" }, + PartyScope: "odrl:PartyScope", + + assignee: { "@type": "@id", "@id": "odrl:assignee" }, + assigner: { "@type": "@id", "@id": "odrl:assigner" }, + assigneeOf: { "@type": "@id", "@id": "odrl:assigneeOf" }, + assignerOf: { "@type": "@id", "@id": "odrl:assignerOf" }, + attributedParty: { "@type": "@id", "@id": "odrl:attributedParty" }, + attributingParty: { "@type": "@id", "@id": "odrl:attributingParty" }, + compensatedParty: { "@type": "@id", "@id": "odrl:compensatedParty" }, + compensatingParty: { "@type": "@id", "@id": "odrl:compensatingParty" }, + consentingParty: { "@type": "@id", "@id": "odrl:consentingParty" }, + consentedParty: { "@type": "@id", "@id": "odrl:consentedParty" }, + informedParty: { "@type": "@id", "@id": "odrl:informedParty" }, + informingParty: { "@type": "@id", "@id": "odrl:informingParty" }, + trackingParty: { "@type": "@id", "@id": "odrl:trackingParty" }, + trackedParty: { "@type": "@id", "@id": "odrl:trackedParty" }, + contractingParty: { "@type": "@id", "@id": "odrl:contractingParty" }, + contractedParty: { "@type": "@id", "@id": "odrl:contractedParty" }, + + Action: "odrl:Action", + action: { "@type": "@vocab", "@id": "odrl:action" }, + includedIn: { "@type": "@id", "@id": "odrl:includedIn" }, + implies: { "@type": "@id", "@id": "odrl:implies" }, + + Permission: "odrl:Permission", + permission: { "@type": "@id", "@id": "odrl:permission" }, + + Prohibition: "odrl:Prohibition", + prohibition: { "@type": "@id", "@id": "odrl:prohibition" }, + + obligation: { "@type": "@id", "@id": "odrl:obligation" }, + + use: "odrl:use", + grantUse: "odrl:grantUse", + aggregate: "odrl:aggregate", + annotate: "odrl:annotate", + anonymize: "odrl:anonymize", + archive: "odrl:archive", + concurrentUse: "odrl:concurrentUse", + derive: "odrl:derive", + digitize: "odrl:digitize", + display: "odrl:display", + distribute: "odrl:distribute", + execute: "odrl:execute", + extract: "odrl:extract", + give: "odrl:give", + index: "odrl:index", + install: "odrl:install", + modify: "odrl:modify", + move: "odrl:move", + play: "odrl:play", + present: "odrl:present", + print: "odrl:print", + read: "odrl:read", + reproduce: "odrl:reproduce", + sell: "odrl:sell", + stream: "odrl:stream", + textToSpeech: "odrl:textToSpeech", + transfer: "odrl:transfer", + transform: "odrl:transform", + translate: "odrl:translate", + + Duty: "odrl:Duty", + duty: { "@type": "@id", "@id": "odrl:duty" }, + consequence: { "@type": "@id", "@id": "odrl:consequence" }, + remedy: { "@type": "@id", "@id": "odrl:remedy" }, + + acceptTracking: "odrl:acceptTracking", + attribute: "odrl:attribute", + compensate: "odrl:compensate", + delete: "odrl:delete", + ensureExclusivity: "odrl:ensureExclusivity", + include: "odrl:include", + inform: "odrl:inform", + nextPolicy: "odrl:nextPolicy", + obtainConsent: "odrl:obtainConsent", + reviewPolicy: "odrl:reviewPolicy", + uninstall: "odrl:uninstall", + watermark: "odrl:watermark", + + Constraint: "odrl:Constraint", + LogicalConstraint: "odrl:LogicalConstraint", + constraint: { "@type": "@id", "@id": "odrl:constraint" }, + refinement: { "@type": "@id", "@id": "odrl:refinement" }, + Operator: "odrl:Operator", + operator: { "@type": "@vocab", "@id": "odrl:operator" }, + RightOperand: "odrl:RightOperand", + rightOperand: "odrl:rightOperand", + rightOperandReference: { + "@type": "xsd:anyURI", + "@id": "odrl:rightOperandReference", + }, + LeftOperand: "odrl:LeftOperand", + leftOperand: { "@type": "@vocab", "@id": "odrl:leftOperand" }, + unit: "odrl:unit", + dataType: { "@type": "xsd:anyType", "@id": "odrl:datatype" }, + status: "odrl:status", + + absolutePosition: "odrl:absolutePosition", + absoluteSpatialPosition: "odrl:absoluteSpatialPosition", + absoluteTemporalPosition: "odrl:absoluteTemporalPosition", + absoluteSize: "odrl:absoluteSize", + count: "odrl:count", + dateTime: "odrl:dateTime", + delayPeriod: "odrl:delayPeriod", + deliveryChannel: "odrl:deliveryChannel", + elapsedTime: "odrl:elapsedTime", + event: "odrl:event", + fileFormat: "odrl:fileFormat", + industry: "odrl:industry:", + language: "odrl:language", + media: "odrl:media", + meteredTime: "odrl:meteredTime", + payAmount: "odrl:payAmount", + percentage: "odrl:percentage", + product: "odrl:product", + purpose: "odrl:purpose", + recipient: "odrl:recipient", + relativePosition: "odrl:relativePosition", + relativeSpatialPosition: "odrl:relativeSpatialPosition", + relativeTemporalPosition: "odrl:relativeTemporalPosition", + relativeSize: "odrl:relativeSize", + resolution: "odrl:resolution", + spatial: "odrl:spatial", + spatialCoordinates: "odrl:spatialCoordinates", + systemDevice: "odrl:systemDevice", + timeInterval: "odrl:timeInterval", + unitOfCount: "odrl:unitOfCount", + version: "odrl:version", + virtualLocation: "odrl:virtualLocation", + + eq: "odrl:eq", + gt: "odrl:gt", + gteq: "odrl:gteq", + lt: "odrl:lt", + lteq: "odrl:lteq", + neq: "odrl:neg", + isA: "odrl:isA", + hasPart: "odrl:hasPart", + isPartOf: "odrl:isPartOf", + isAllOf: "odrl:isAllOf", + isAnyOf: "odrl:isAnyOf", + isNoneOf: "odrl:isNoneOf", + or: "odrl:or", + xone: "odrl:xone", + and: "odrl:and", + andSequence: "odrl:andSequence", + + policyUsage: "odrl:policyUsage", + }, +}; diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index 124a8ac2..e3331b06 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -32,9 +32,8 @@ import type { import { ContextParser, FetchDocumentLoader } from "jsonld-context-parser"; import { JsonLdParser } from "jsonld-streaming-parser"; import { Store } from "n3"; -import { ParsingContext } from "jsonld-streaming-parser/lib/ParsingContext"; -import CONTEXTS, { cachedContexts } from "./contexts"; import type { JsonLd } from "../common/common"; +import CONTEXTS, { cachedContexts } from "./contexts"; /** * A JSON-LD document loader with the standard context for VCs pre-loaded @@ -83,8 +82,6 @@ export interface ParseOptions { allowContextFetching?: boolean; } -const reusableDocumentLoader = new CachedFetchDocumentLoader(); - class MyContextParser extends ContextParser { private cachedParsing: Record> = {}; @@ -105,9 +102,8 @@ class MyContextParser extends ContextParser { context.every((c) => typeof c === "string") ) { const str = JSON.stringify(context); - console.log("cache hit", str in this.cachedParsing); + // eslint-disable-next-line no-return-assign return (this.cachedParsing[str] ??= super.parse(context, options)); - // return super.parse(context, options) } if (!Array.isArray(context)) { if (!this.parentContexts.has(options?.parentContext)) { @@ -127,9 +123,8 @@ class MyContextParser extends ContextParser { } } -const reusableContextParser = new MyContextParser({ - documentLoader: reusableDocumentLoader, -}); +let reusableDocumentLoader: CachedFetchDocumentLoader; +let reusableContextParser: MyContextParser; /** * Our internal JsonLd Parser with a cached VC context @@ -139,6 +134,7 @@ export class CachedJsonLdParser extends JsonLdParser { let documentLoader: CachedFetchDocumentLoader; if (!options?.contexts && !options?.allowContextFetching) { + reusableDocumentLoader ??= new CachedFetchDocumentLoader(); documentLoader = reusableDocumentLoader; } else { documentLoader = new CachedFetchDocumentLoader( @@ -154,7 +150,10 @@ export class CachedJsonLdParser extends JsonLdParser { }); if (!options?.contexts && !options?.allowContextFetching) { - // @ts-ignore + reusableContextParser ??= new MyContextParser({ + documentLoader: reusableDocumentLoader, + }); + // @ts-expect-error parsingContext is an internal property this.parsingContext.contextParser = reusableContextParser; } } From 36c658f63acd8fe9e676d178f6e0a34669fb2688 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:00:18 +0100 Subject: [PATCH 14/79] chore: generalize caching behavior --- package-lock.json | 39 ++++++++++++++++++++++ package.json | 2 ++ src/lookup/query.ts | 19 ++++++++--- src/parser/jsonld.ts | 78 +++++++++++++++++++++++++++++--------------- 4 files changed, 106 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0cb6c551..4f28caea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "jsonld-context-parser": "^2.3.3", "jsonld-streaming-parser": "^3.2.1", "jsonld-streaming-serializer": "^2.1.0", + "md5": "^2.3.0", "n3": "^1.17.0" }, "devDependencies": { @@ -30,6 +31,7 @@ "@types/content-type": "^1.1.5", "@types/dotenv-flow": "^3.1.1", "@types/jest": "^29.2.2", + "@types/md5": "^2.3.4", "@types/n3": "^1.16.0", "@types/node": "^20.1.2", "@types/rdfjs__dataset": "2.0.5", @@ -1923,6 +1925,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/md5": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.4.tgz", + "integrity": "sha512-e/L4hvpCK8GavKXmP02QlNilZOj8lpmZGGA9QGMMPZjCUoKgi1B4BvhXcbruIi6r+PqzpcjLfda/tocpHFKqDA==", + "dev": true + }, "node_modules/@types/n3": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.16.3.tgz", @@ -2996,6 +3004,14 @@ "node": ">=10" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/chromium-bidi": { "version": "0.4.16", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", @@ -3226,6 +3242,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/crypto-js": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", @@ -5475,6 +5499,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -6757,6 +6786,16 @@ "node": ">= 12" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", diff --git a/package.json b/package.json index 7335ef1a..a61faea5 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@types/content-type": "^1.1.5", "@types/dotenv-flow": "^3.1.1", "@types/jest": "^29.2.2", + "@types/md5": "^2.3.4", "@types/n3": "^1.16.0", "@types/node": "^20.1.2", "@types/rdfjs__dataset": "2.0.5", @@ -93,6 +94,7 @@ "jsonld-context-parser": "^2.3.3", "jsonld-streaming-parser": "^3.2.1", "jsonld-streaming-serializer": "^2.1.0", + "md5": "^2.3.0", "n3": "^1.17.0" }, "publishConfig": { diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 4ebd63be..19c5504b 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -145,11 +145,20 @@ export async function query( } if (data.verifiableCredential) { - data.verifiableCredential = await Promise.all( - data.verifiableCredential.map((vc) => - verifiableCredentialToDataset(vc, options), - ), - ); + const newVerifiableCredential: (VerifiableCredential & DatasetCore)[] = []; + for (let i = 0; i < data.verifiableCredential.length; i += 500) { + console.time(i.toString()); + newVerifiableCredential.push( + // eslint-disable-next-line no-await-in-loop + ...(await Promise.all( + data.verifiableCredential + .slice(i, i + 500) + .map((vc) => verifiableCredentialToDataset(vc, options)), + )), + ); + console.timeEnd(i.toString()); + } + data.verifiableCredential = newVerifiableCredential; } return data as ParsedVerifiablePresentation; } diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index e3331b06..f50d8159 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -24,7 +24,6 @@ import { fetch as defaultFetch } from "@inrupt/universal-fetch"; import { promisifyEventEmitter } from "event-emitter-promisify"; import type { IJsonLdContext, - IJsonLdContextNormalizedRaw, IParseOptions, JsonLdContext, JsonLdContextNormalized, @@ -32,6 +31,7 @@ import type { import { ContextParser, FetchDocumentLoader } from "jsonld-context-parser"; import { JsonLdParser } from "jsonld-streaming-parser"; import { Store } from "n3"; +import md5 from "md5"; import type { JsonLd } from "../common/common"; import CONTEXTS, { cachedContexts } from "./contexts"; @@ -82,44 +82,68 @@ export interface ParseOptions { allowContextFetching?: boolean; } +function hashOptions(options: IParseOptions | undefined) { + const opts = { ...options, parentContext: undefined }; + for (const key of Object.keys(opts)) { + if (typeof opts[key as keyof typeof opts] === "undefined") { + delete opts[key as keyof typeof opts]; + } + } + + return md5(JSON.stringify(opts, Object.keys(opts).sort())); +} + +function hashContext( + context: JsonLdContext, + cmap: (c: IJsonLdContext) => number, +): string { + if (Array.isArray(context)) { + return md5( + JSON.stringify(context.map((c) => (typeof c === "string" ? c : cmap(c)))), + ); + } + if (typeof context === "string") { + return md5(context); + } + return cmap(context).toString(); +} + class MyContextParser extends ContextParser { private cachedParsing: Record> = {}; - private parentContexts = new Map< - IJsonLdContextNormalizedRaw | undefined, - Map> - >(); + private contextMap: Map = new Map(); + + private contextHashMap: Map = new Map(); + + private mapIndex = 1; + + private cmap = (context: IJsonLdContext) => { + if (!this.contextMap.has(context)) { + const hash = md5(JSON.stringify(context)); + if (!this.contextHashMap.has(hash)) { + this.contextHashMap.set(hash, (this.mapIndex += 1)); + } + this.contextMap.set(context, this.contextHashMap.get(hash)!); + } + return this.contextMap.get(context)!; + }; async parse( context: JsonLdContext, options?: IParseOptions, ): Promise { + let hash = hashOptions(options); + if ( - typeof options?.baseIRI === "undefined" && - options?.processingMode === 1.1 && - Object.keys(options?.parentContext ?? {}).length === 0 && - Array.isArray(context) && - context.every((c) => typeof c === "string") + options?.parentContext && + Object.keys(options?.parentContext ?? {}).length !== 0 ) { - const str = JSON.stringify(context); - // eslint-disable-next-line no-return-assign - return (this.cachedParsing[str] ??= super.parse(context, options)); - } - if (!Array.isArray(context)) { - if (!this.parentContexts.has(options?.parentContext)) { - this.parentContexts.set(options?.parentContext, new Map()); - } - - const childContext = this.parentContexts.get(options?.parentContext)!; - - if (!childContext.has(context)) { - childContext.set(context, super.parse(context, options)); - } - - return childContext.get(context)!; + hash = md5(hash + this.cmap(options.parentContext)); } - return super.parse(context, options); + // eslint-disable-next-line no-return-assign + return (this.cachedParsing[md5(hash + hashContext(context, this.cmap))] ??= + super.parse(context, options)); } } From 1ac2c055af1c85046c9ad2cf80f8f404bf33620b Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:02:39 +0100 Subject: [PATCH 15/79] chore: reduce batch size for vc parsing --- src/lookup/query.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 19c5504b..c5de580e 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -146,13 +146,13 @@ export async function query( if (data.verifiableCredential) { const newVerifiableCredential: (VerifiableCredential & DatasetCore)[] = []; - for (let i = 0; i < data.verifiableCredential.length; i += 500) { + for (let i = 0; i < data.verifiableCredential.length; i += 100) { console.time(i.toString()); newVerifiableCredential.push( // eslint-disable-next-line no-await-in-loop ...(await Promise.all( data.verifiableCredential - .slice(i, i + 500) + .slice(i, i + 100) .map((vc) => verifiableCredentialToDataset(vc, options)), )), ); From 501a30e0ea34711fd872b69f6fdc4bb29019ba10 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:04:29 +0100 Subject: [PATCH 16/79] chore: use console timing --- src/lookup/query.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lookup/query.ts b/src/lookup/query.ts index c5de580e..1a1fa61c 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -147,7 +147,6 @@ export async function query( if (data.verifiableCredential) { const newVerifiableCredential: (VerifiableCredential & DatasetCore)[] = []; for (let i = 0; i < data.verifiableCredential.length; i += 100) { - console.time(i.toString()); newVerifiableCredential.push( // eslint-disable-next-line no-await-in-loop ...(await Promise.all( @@ -156,7 +155,6 @@ export async function query( .map((vc) => verifiableCredentialToDataset(vc, options)), )), ); - console.timeEnd(i.toString()); } data.verifiableCredential = newVerifiableCredential; } From 0f8ce276b6ea8a977b9d2ea189bc92385ef44b48 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:57:12 +0100 Subject: [PATCH 17/79] chore: collect jest coverage --- src/common/common.test.ts | 40 +++++++++++++ src/parser/jsonld.test.ts | 123 ++++++++++++++++++++++++++++---------- src/parser/jsonld.ts | 36 ++++------- 3 files changed, 144 insertions(+), 55 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 051fd26c..9c08eac0 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -23,6 +23,7 @@ import { Response, fetch as uniFetch } from "@inrupt/universal-fetch"; import { describe, expect, it, jest, beforeEach } from "@jest/globals"; import { DataFactory } from "n3"; import { isomorphic } from "rdf-isomorphic"; +import type { IJsonLdContext } from "jsonld-context-parser"; import { jsonLdStringToStore } from "../parser/jsonld"; import type { VerifiableCredential } from "./common"; import { @@ -605,6 +606,45 @@ describe("getVerifiableCredential", () => { expect(JSON.parse(JSON.stringify(vc))).toEqual(mocked); }); + it("should handle credential subjects with multiple objects and a custom context", async () => { + const mocked = mockDefaultCredential(); + mocked["@context"] = [ + ...(mocked["@context"] as (IJsonLdContext | string)[]), + { + ex: "http://example.org/", + }, + ]; + mocked.credentialSubject = { + ...mocked.credentialSubject, + "https://example.org/ns/passengerOf": [ + { "@id": "http://example.org/v1" }, + { "@id": "http://example.org/v2" }, + ], + "https://example.org/my/predicate/i": { + "https://example.org/my/predicate": "object", + }, + }; + + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + + const vc = await getVerifiableCredential("https://some.vc", { + fetch: mockedFetch, + }); + + // Since we have dataset properties in vc it should match the result + // but won't equal + expect(vc).toMatchObject(mocked); + // However we DO NOT want these properties showing up when we stringify + // the VC + expect(JSON.parse(JSON.stringify(vc))).toEqual(mocked); + }); + it("throws if there are 2 proof values", async () => { const mocked = mockDefaultCredential(); // @ts-expect-error proofValue is a string not string[] in VC type diff --git a/src/parser/jsonld.test.ts b/src/parser/jsonld.test.ts index 7a3b8499..94373f11 100644 --- a/src/parser/jsonld.test.ts +++ b/src/parser/jsonld.test.ts @@ -20,11 +20,14 @@ // import type * as UniversalFetch from "@inrupt/universal-fetch"; -import { beforeAll, describe, expect, it, jest } from "@jest/globals"; -import type { JsonLdContextNormalized } from "jsonld-context-parser"; +import { describe, expect, it, jest } from "@jest/globals"; import { DataFactory as DF } from "n3"; import { isomorphic } from "rdf-isomorphic"; -import { getVcContext, jsonLdToStore } from "./jsonld"; +import { + CachedFetchDocumentLoader, + CachingContextParser, + jsonLdToStore, +} from "./jsonld"; jest.mock("@inrupt/universal-fetch", () => { const fetchModule = jest.requireActual( @@ -62,41 +65,101 @@ describe("jsonLdToStore", () => { }); }); -describe("getVcContext", () => { - let context: JsonLdContextNormalized; +describe("CachingContextParser", () => { + it("should return the same object parsing the same context multiple times", async () => { + const contextParser = new CachingContextParser({ + documentLoader: new CachedFetchDocumentLoader(), + }); + await expect( + contextParser.parse("https://w3id.org/vc/status-list/2021/v1"), + ).resolves.toEqual( + await contextParser.parse("https://w3id.org/vc/status-list/2021/v1"), + ); - beforeAll(async () => { - context = await getVcContext(); - }); + await expect( + contextParser.parse({ ex: "http://example.org" }), + ).resolves.toEqual(await contextParser.parse({ ex: "http://example.org" })); - it("should be able to compact and expand IRIs from the VC context", () => { - expect( - context.compactIri( - "https://www.w3.org/2018/credentials#VerifiableCredential", - true, + await expect( + contextParser.parse([ + "https://w3id.org/vc/status-list/2021/v1", + { ex: "http://example.org" }, + ]), + ).resolves.toEqual( + await contextParser.parse([ + "https://w3id.org/vc/status-list/2021/v1", + { ex: "http://example.org" }, + ]), + ); + + await expect( + contextParser.parse( + [ + "https://w3id.org/vc/status-list/2021/v1", + { ex: "http://example.org" }, + ], + { + parentContext: undefined, + }, ), - ).toBe("VerifiableCredential"); - expect(context.expandTerm("VerifiableCredential", true)).toBe( - "https://www.w3.org/2018/credentials#VerifiableCredential", + ).resolves.toEqual( + await contextParser.parse([ + "https://w3id.org/vc/status-list/2021/v1", + { ex: "http://example.org" }, + ]), ); - }); - it("should be able to compact and expand IRIs from the Inrupt context", () => { - expect(context.compactIri("https://w3id.org/GConsent#Consent", true)).toBe( - "Consent", + await expect( + contextParser.parse( + [ + "https://w3id.org/vc/status-list/2021/v1", + { ex: "http://example.org" }, + ], + { + parentContext: {}, + }, + ), + ).resolves.toEqual( + await contextParser.parse([ + "https://w3id.org/vc/status-list/2021/v1", + { ex: "http://example.org" }, + ]), ); - expect(context.expandTerm("Consent", true)).toBe( - "https://w3id.org/GConsent#Consent", + + await expect( + contextParser.parse([ + "https://w3id.org/vc/status-list/2021/v1", + { ex: "http://example.org" }, + ]), + ).resolves.toEqual( + await contextParser.parse( + [ + "https://w3id.org/vc/status-list/2021/v1", + { ex: "http://example.org" }, + ], + { + parentContext: {}, + }, + ), ); - }); - it("should not compact and expand IRIs not in the VC or Inrupt context", () => { - expect( - context.compactIri( - "https://example.org/credentials#VerifiableCredential", - true, + await expect( + contextParser.parse([ + "https://w3id.org/vc/status-list/2021/v1", + { ex: "http://example.org" }, + ]), + ).resolves.not.toEqual( + await contextParser.parse( + [ + "https://w3id.org/vc/status-list/2021/v1", + { ex: "http://example.org" }, + ], + { + parentContext: { + "@base": "http://example.org/test", + }, + }, ), - ).toBe("https://example.org/credentials#VerifiableCredential"); - expect(context.expandTerm("VC", true)).toBe("VC"); + ); }); }); diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index f50d8159..f10f47fe 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -38,7 +38,7 @@ import CONTEXTS, { cachedContexts } from "./contexts"; /** * A JSON-LD document loader with the standard context for VCs pre-loaded */ -class CachedFetchDocumentLoader extends FetchDocumentLoader { +export class CachedFetchDocumentLoader extends FetchDocumentLoader { private contexts: Record; constructor( @@ -61,21 +61,6 @@ class CachedFetchDocumentLoader extends FetchDocumentLoader { } } -/** - * Creates a context for use with the VC library - */ -export function getVcContext( - ...contexts: IJsonLdContext[] -): Promise { - const myParser = new ContextParser({ - documentLoader: new CachedFetchDocumentLoader(), - }); - return myParser.parse([ - ...Object.values(CONTEXTS).map((x) => x["@context"]), - ...contexts, - ]); -} - export interface ParseOptions { baseIRI?: string; contexts?: Record; @@ -102,13 +87,10 @@ function hashContext( JSON.stringify(context.map((c) => (typeof c === "string" ? c : cmap(c)))), ); } - if (typeof context === "string") { - return md5(context); - } - return cmap(context).toString(); + return typeof context === "string" ? md5(context) : cmap(context).toString(); } -class MyContextParser extends ContextParser { +export class CachingContextParser extends ContextParser { private cachedParsing: Record> = {}; private contextMap: Map = new Map(); @@ -136,7 +118,7 @@ class MyContextParser extends ContextParser { if ( options?.parentContext && - Object.keys(options?.parentContext ?? {}).length !== 0 + Object.keys(options.parentContext).length !== 0 ) { hash = md5(hash + this.cmap(options.parentContext)); } @@ -148,7 +130,7 @@ class MyContextParser extends ContextParser { } let reusableDocumentLoader: CachedFetchDocumentLoader; -let reusableContextParser: MyContextParser; +let reusableContextParser: CachingContextParser; /** * Our internal JsonLd Parser with a cached VC context @@ -158,7 +140,11 @@ export class CachedJsonLdParser extends JsonLdParser { let documentLoader: CachedFetchDocumentLoader; if (!options?.contexts && !options?.allowContextFetching) { - reusableDocumentLoader ??= new CachedFetchDocumentLoader(); + reusableDocumentLoader ??= new CachedFetchDocumentLoader( + undefined, + undefined, + defaultFetch, + ); documentLoader = reusableDocumentLoader; } else { documentLoader = new CachedFetchDocumentLoader( @@ -174,7 +160,7 @@ export class CachedJsonLdParser extends JsonLdParser { }); if (!options?.contexts && !options?.allowContextFetching) { - reusableContextParser ??= new MyContextParser({ + reusableContextParser ??= new CachingContextParser({ documentLoader: reusableDocumentLoader, }); // @ts-expect-error parsingContext is an internal property From 17df529b40ab685bc6604437579a4f4a041d7d37 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 31 Oct 2023 10:54:40 +0000 Subject: [PATCH 18/79] Update src/lookup/query.ts --- src/lookup/query.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 1a1fa61c..5a736f5c 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -148,6 +148,8 @@ export async function query( const newVerifiableCredential: (VerifiableCredential & DatasetCore)[] = []; for (let i = 0; i < data.verifiableCredential.length; i += 100) { newVerifiableCredential.push( + // Limit concurrency to avoid memory overflows. For details see + // https://github.com/inrupt/solid-client-vc-js/pull/849#discussion_r1377400688 // eslint-disable-next-line no-await-in-loop ...(await Promise.all( data.verifiableCredential From 83a182b631fd036835304ae92eb42a1eb9e48642 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 31 Oct 2023 10:58:18 +0000 Subject: [PATCH 19/79] Update src/lookup/query.ts --- src/lookup/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 5a736f5c..d1b20cdb 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -145,7 +145,7 @@ export async function query( } if (data.verifiableCredential) { - const newVerifiableCredential: (VerifiableCredential & DatasetCore)[] = []; + const newVerifiableCredential: NonNullable = []; for (let i = 0; i < data.verifiableCredential.length; i += 100) { newVerifiableCredential.push( // Limit concurrency to avoid memory overflows. For details see From e953cff39daf431600eed4e2739923695a7bce5d Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 31 Oct 2023 10:59:45 +0000 Subject: [PATCH 20/79] Update src/lookup/query.ts --- src/lookup/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lookup/query.ts b/src/lookup/query.ts index d1b20cdb..e18a1911 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -145,7 +145,7 @@ export async function query( } if (data.verifiableCredential) { - const newVerifiableCredential: NonNullable = []; + const newVerifiableCredential: NonNullable = []; for (let i = 0; i < data.verifiableCredential.length; i += 100) { newVerifiableCredential.push( // Limit concurrency to avoid memory overflows. For details see From 64d0c25efa8614867ed7ecb1cd0eb25c9d3ea4e9 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:19:37 +0000 Subject: [PATCH 21/79] chore: update context parser and streaming parser --- package-lock.json | 19 +++++++++---------- package.json | 4 ++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f28caea..7ed0e785 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,8 @@ "@inrupt/universal-fetch": "^1.0.1", "content-type": "^1.0.5", "event-emitter-promisify": "^1.1.0", - "jsonld-context-parser": "^2.3.3", - "jsonld-streaming-parser": "^3.2.1", + "jsonld-context-parser": "^2.4.0", + "jsonld-streaming-parser": "^3.3.0", "jsonld-streaming-serializer": "^2.1.0", "md5": "^2.3.0", "n3": "^1.17.0" @@ -6616,13 +6616,12 @@ } }, "node_modules/jsonld-context-parser": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/jsonld-context-parser/-/jsonld-context-parser-2.3.3.tgz", - "integrity": "sha512-H+REInOx7XI2ciF8wJV31D20Bh+ofBmEjN2Tkds51vypqDJIiD341E5g+hYyrEInIKRnbW58TN/Ehz+ACT0l0w==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonld-context-parser/-/jsonld-context-parser-2.4.0.tgz", + "integrity": "sha512-ZYOfvh525SdPd9ReYY58dxB3E2RUEU4DJ6ZibO8AitcowPeBH4L5rCAitE2om5G1P+HMEgYEYEr4EZKbVN4tpA==", "dependencies": { "@types/http-link-header": "^1.0.1", "@types/node": "^18.0.0", - "canonicalize": "^1.0.1", "cross-fetch": "^3.0.6", "http-link-header": "^1.0.2", "relative-to-absolute-iri": "^1.0.5" @@ -6637,9 +6636,9 @@ "integrity": "sha512-xNbS75FxH6P4UXTPUJp/zNPq6/xsfdJKussCWNOnz4aULWIRwMgP1LgaB5RiBnMX1DPCYenuqGZfnIAx5mbFLA==" }, "node_modules/jsonld-streaming-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonld-streaming-parser/-/jsonld-streaming-parser-3.2.1.tgz", - "integrity": "sha512-MZCUrQe3pBO2pk2i3BpyW9Yn2oZoe2RCRpHZAJa88S6tRyxbe7XcjWfTKAZv35obDJDIREgot4723VhbClJELw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jsonld-streaming-parser/-/jsonld-streaming-parser-3.3.0.tgz", + "integrity": "sha512-6aWiAsWGZioTB/vNQ3KenREz9ddEOliZoEETi+jLrlL7+vkgMeHjnxyFlGe4UOCU7SVUNPhz/lgLGZjnxgVYtA==", "dependencies": { "@bergos/jsonparse": "^1.4.0", "@rdfjs/types": "*", @@ -6648,7 +6647,7 @@ "buffer": "^6.0.3", "canonicalize": "^1.0.1", "http-link-header": "^1.0.2", - "jsonld-context-parser": "^2.3.0", + "jsonld-context-parser": "^2.4.0", "rdf-data-factory": "^1.1.0", "readable-stream": "^4.0.0" } diff --git a/package.json b/package.json index a61faea5..a63fc161 100644 --- a/package.json +++ b/package.json @@ -91,8 +91,8 @@ "@inrupt/universal-fetch": "^1.0.1", "content-type": "^1.0.5", "event-emitter-promisify": "^1.1.0", - "jsonld-context-parser": "^2.3.3", - "jsonld-streaming-parser": "^3.2.1", + "jsonld-context-parser": "^2.4.0", + "jsonld-streaming-parser": "^3.3.0", "jsonld-streaming-serializer": "^2.1.0", "md5": "^2.3.0", "n3": "^1.17.0" From 97164a098ce1db312ec15c156abfbd0f8b4a1cc0 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:24:21 +0000 Subject: [PATCH 22/79] chore: fix lint errors --- src/lookup/query.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lookup/query.ts b/src/lookup/query.ts index e18a1911..2cd28d7a 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -145,7 +145,9 @@ export async function query( } if (data.verifiableCredential) { - const newVerifiableCredential: NonNullable = []; + const newVerifiableCredential: NonNullable< + ParsedVerifiablePresentation["verifiableCredential"] + > = []; for (let i = 0; i < data.verifiableCredential.length; i += 100) { newVerifiableCredential.push( // Limit concurrency to avoid memory overflows. For details see From e109c3c13251407222b75824db17a1655e39a123 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:30:05 +0000 Subject: [PATCH 23/79] chore: rebuild lockfile --- package-lock.json | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb2d95fd..f244c449 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1840,9 +1840,9 @@ } }, "node_modules/@types/content-type": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.7.tgz", - "integrity": "sha512-dSM/IQ1fgM1aSQ2PlHR4uWQGDjs9SY+/ilm228CIs9hwlyIfW+q5asThulBforWR6ktt/o8L8m6GPW/Fz1dk2A==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.8.tgz", + "integrity": "sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg==", "dev": true }, "node_modules/@types/dotenv-flow": { @@ -1926,15 +1926,15 @@ "dev": true }, "node_modules/@types/md5": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.4.tgz", - "integrity": "sha512-e/L4hvpCK8GavKXmP02QlNilZOj8lpmZGGA9QGMMPZjCUoKgi1B4BvhXcbruIi6r+PqzpcjLfda/tocpHFKqDA==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.5.tgz", + "integrity": "sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==", "dev": true }, "node_modules/@types/n3": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.16.3.tgz", - "integrity": "sha512-6PDn2/tUtveGQONiFJDOpl2Aa/YnmwQhdQ8SznUJVFmjI/NEBBdNhnkmYeEFmvOQbhbIeGR+SfmTk71TdqJ5mg==", + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/@types/n3/-/n3-1.16.4.tgz", + "integrity": "sha512-6PmHRYCCdjbbBV2UVC/HjtL6/5Orx9ku2CQjuojucuHvNvPmnm6+02B18YGhHfvU25qmX2jPXyYPHsMNkn+w2w==", "dev": true, "dependencies": { "@rdfjs/types": "^1.1.0", @@ -5217,6 +5217,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", From 0ab437179feeb913aae30b536ef015ccf7520865 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:50:50 +0000 Subject: [PATCH 24/79] chore: update type export --- src/common/common.mock.ts | 20 +++++++++------- src/common/common.ts | 49 +++++++++++++++++++++------------------ src/index.test.ts | 5 ++++ src/index.ts | 4 ++++ src/issue/issue.ts | 9 ++++--- src/lookup/derive.ts | 15 +++++++----- src/lookup/query.ts | 6 ++--- src/verify/verify.ts | 10 ++++---- 8 files changed, 68 insertions(+), 50 deletions(-) diff --git a/src/common/common.mock.ts b/src/common/common.mock.ts index 0bac7662..a3463a4d 100644 --- a/src/common/common.mock.ts +++ b/src/common/common.mock.ts @@ -21,7 +21,7 @@ import type { Iri, - VerifiableCredential, + VerifiableCredentialBase, VerifiablePresentation, } from "./common"; import { defaultCredentialTypes } from "./common"; @@ -114,28 +114,30 @@ export const mockPartialCredential2Proofs = ( export const mockCredential = ( claims: CredentialClaims, -): VerifiableCredential => { - return mockPartialCredential(claims) as VerifiableCredential; +): VerifiableCredentialBase => { + return mockPartialCredential(claims) as VerifiableCredentialBase; }; -export const mockDefaultCredential = (id?: string): VerifiableCredential => { +export const mockDefaultCredential = ( + id?: string, +): VerifiableCredentialBase => { return mockPartialCredential( defaultCredentialClaims, id, - ) as VerifiableCredential; + ) as VerifiableCredentialBase; }; export const mockDefaultCredential2Proofs = ( id?: string, -): VerifiableCredential => { +): VerifiableCredentialBase => { return mockPartialCredential2Proofs( defaultCredentialClaims, id, - ) as VerifiableCredential; + ) as VerifiableCredentialBase; }; export const mockPartialPresentation = ( - credentials: VerifiableCredential[], + credentials: VerifiableCredentialBase[], claims?: Partial, ): Record => { return { @@ -153,7 +155,7 @@ export const mockPartialPresentation = ( }; export const mockDefaultPresentation = ( - vc: VerifiableCredential[] = [mockDefaultCredential()], + vc: VerifiableCredentialBase[] = [mockDefaultCredential()], ): VerifiablePresentation => { return mockPartialPresentation( vc, diff --git a/src/common/common.ts b/src/common/common.ts index 2ee46b60..1cf6fa9d 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -59,7 +59,7 @@ type Proof = { /** * A Verifiable Credential JSON-LD document, as specified by the W3C VC HTTP API. */ -export type VerifiableCredential = JsonLd & { +export type VerifiableCredentialBase = JsonLd & { id: Iri; type: Iri[]; issuer: Iri; @@ -80,10 +80,12 @@ export type VerifiableCredential = JsonLd & { proof: Proof; }; +export type VerifiableCredential = VerifiableCredentialBase & DatasetCore; + export type VerifiablePresentation = JsonLd & { id?: string; type: string | string[]; - verifiableCredential?: VerifiableCredential[]; + verifiableCredential?: VerifiableCredentialBase[]; holder?: string; proof?: Proof; }; @@ -157,43 +159,46 @@ export function normalizeVp(vpJson: T): T { * @returns true is the payload matches our expectation. */ export function isVerifiableCredential( - data: unknown | VerifiableCredential, -): data is VerifiableCredential { + data: unknown | VerifiableCredentialBase, +): data is VerifiableCredentialBase { let dataIsVc = true; - dataIsVc = typeof (data as VerifiableCredential).id === "string"; - dataIsVc = dataIsVc && Array.isArray((data as VerifiableCredential).type); + dataIsVc = typeof (data as VerifiableCredentialBase).id === "string"; + dataIsVc = dataIsVc && Array.isArray((data as VerifiableCredentialBase).type); dataIsVc = - dataIsVc && typeof (data as VerifiableCredential).issuer === "string"; + dataIsVc && typeof (data as VerifiableCredentialBase).issuer === "string"; dataIsVc = - dataIsVc && typeof (data as VerifiableCredential).issuanceDate === "string"; + dataIsVc && + typeof (data as VerifiableCredentialBase).issuanceDate === "string"; dataIsVc = dataIsVc && - !Number.isNaN(Date.parse((data as VerifiableCredential).issuanceDate)); + !Number.isNaN(Date.parse((data as VerifiableCredentialBase).issuanceDate)); dataIsVc = dataIsVc && - typeof (data as VerifiableCredential).credentialSubject === "object"; + typeof (data as VerifiableCredentialBase).credentialSubject === "object"; dataIsVc = dataIsVc && - typeof (data as VerifiableCredential).credentialSubject.id === "string"; + typeof (data as VerifiableCredentialBase).credentialSubject.id === "string"; dataIsVc = - dataIsVc && typeof (data as VerifiableCredential).proof === "object"; + dataIsVc && typeof (data as VerifiableCredentialBase).proof === "object"; dataIsVc = dataIsVc && - typeof (data as VerifiableCredential).proof.created === "string"; + typeof (data as VerifiableCredentialBase).proof.created === "string"; dataIsVc = dataIsVc && - !Number.isNaN(Date.parse((data as VerifiableCredential).proof.created)); + !Number.isNaN(Date.parse((data as VerifiableCredentialBase).proof.created)); dataIsVc = dataIsVc && - typeof (data as VerifiableCredential).proof.proofPurpose === "string"; + typeof (data as VerifiableCredentialBase).proof.proofPurpose === "string"; dataIsVc = dataIsVc && - typeof (data as VerifiableCredential).proof.proofValue === "string"; + typeof (data as VerifiableCredentialBase).proof.proofValue === "string"; dataIsVc = - dataIsVc && typeof (data as VerifiableCredential).proof.type === "string"; + dataIsVc && + typeof (data as VerifiableCredentialBase).proof.type === "string"; dataIsVc = dataIsVc && - typeof (data as VerifiableCredential).proof.verificationMethod === "string"; + typeof (data as VerifiableCredentialBase).proof.verificationMethod === + "string"; return dataIsVc; } @@ -403,9 +408,9 @@ export async function getVerifiableCredentialApiConfiguration( * @hidden */ export async function verifiableCredentialToDataset( - vc: VerifiableCredential, + vc: VerifiableCredentialBase, options?: ParseOptions, -): Promise { +): Promise { let store: DatasetCore; try { store = await jsonLdToStore(vc, options); @@ -461,7 +466,7 @@ export async function getVerifiableCredential( options?: ParseOptions & { fetch?: typeof fetch; }, -): Promise { +): Promise { const authFetch = options?.fetch ?? uniFetch; const response = await authFetch(vcUrl); @@ -471,7 +476,7 @@ export async function getVerifiableCredential( ); } - let vc: unknown | VerifiableCredential; + let vc: unknown | VerifiableCredentialBase; try { vc = normalizeVc(await response.json()); } catch (e) { diff --git a/src/index.test.ts b/src/index.test.ts index d7732fa0..5b18a034 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -26,6 +26,7 @@ import { isVerifiablePresentation, getVerifiableCredential, getVerifiableCredentialApiConfiguration, + verifiableCredentialToDataset, } from "./common/common"; import getVerifiableCredentialAllFromShape from "./lookup/derive"; import revokeVerifiableCredential from "./revoke/revoke"; @@ -40,6 +41,7 @@ describe("exports", () => { "isVerifiablePresentation", "getVerifiableCredential", "getVerifiableCredentialApiConfiguration", + "verifiableCredentialToDataset", "getVerifiableCredentialAllFromShape", "query", "revokeVerifiableCredential", @@ -62,6 +64,9 @@ describe("exports", () => { expect(packageExports.getVerifiableCredentialAllFromShape).toBe( getVerifiableCredentialAllFromShape, ); + expect(packageExports.verifiableCredentialToDataset).toBe( + verifiableCredentialToDataset, + ); expect(packageExports.revokeVerifiableCredential).toBe( revokeVerifiableCredential, ); diff --git a/src/index.ts b/src/index.ts index e98d64f9..a7a7cadc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,10 @@ export { isVerifiablePresentation, getVerifiableCredential, getVerifiableCredentialApiConfiguration, + /** + * @hidden @deprecated + */ + verifiableCredentialToDataset, } from "./common/common"; export { default as getVerifiableCredentialAllFromShape } from "./lookup/derive"; export { query } from "./lookup/query"; diff --git a/src/issue/issue.ts b/src/issue/issue.ts index 4aa258c2..4f89b51f 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -25,7 +25,6 @@ import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; -import type { DatasetCore } from "@rdfjs/types"; import type { Iri, JsonLd, VerifiableCredential } from "../common/common"; import { concatenateContexts, @@ -50,7 +49,7 @@ async function internal_issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: OptionsType & ParseOptions, -): Promise { +): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { internalOptions.fetch = fallbackFetch; @@ -138,7 +137,7 @@ export async function issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: OptionsType, -): Promise; +): Promise; /** * @deprecated Please remove the `subjectId` parameter */ @@ -148,7 +147,7 @@ export async function issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: OptionsType, -): Promise; +): Promise; // The signature of the implementation here is a bit confusing, but it avoid // breaking changes until we remove the `subjectId` completely from the API export async function issueVerifiableCredential( @@ -157,7 +156,7 @@ export async function issueVerifiableCredential( subjectOrCredentialClaims: JsonLd | undefined, credentialClaimsOrOptions?: JsonLd | OptionsType, options?: OptionsType, -): Promise { +): Promise { if (typeof subjectIdOrClaims === "string") { // The function has been called with the deprecated signature, and the // subjectOrCredentialClaims parameter should be ignored. diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index f35af94a..300fa581 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -20,8 +20,11 @@ // import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; -import type { DatasetCore } from "@rdfjs/types"; -import type { Iri, VerifiableCredential } from "../common/common"; +import type { + Iri, + VerifiableCredential, + VerifiableCredentialBase, +} from "../common/common"; import { concatenateContexts, defaultContext } from "../common/common"; import type { VerifiablePresentationRequest } from "./query"; import { query } from "./query"; @@ -37,7 +40,7 @@ const INCLUDE_EXPIRED_VC_OPTION = "ExpiredVerifiableCredential" as const; * @returns A legacy object expected by the /derive endpoint of the ESS 2.0 VC service */ function buildLegacyQuery( - vcShape: Partial, + vcShape: Partial, includeExpiredVc: boolean, ) { // credentialClaims should contain all the claims, but not the context. @@ -60,7 +63,7 @@ function buildLegacyQuery( * @returns A Query by Example VP Request based on the provided example. */ function buildQueryByExample( - vcShape: Partial, + vcShape: Partial, ): VerifiablePresentationRequest { return { query: [ @@ -96,12 +99,12 @@ function buildQueryByExample( */ export async function getVerifiableCredentialAllFromShape( holderEndpoint: Iri, - vcShape: Partial, + vcShape: Partial, options?: Partial<{ fetch: typeof fallbackFetch; includeExpiredVc: boolean; }>, -): Promise<(VerifiableCredential & DatasetCore)[]> { +): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { internalOptions.fetch = fallbackFetch; diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 2cd28d7a..c33e9417 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -20,10 +20,10 @@ // import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; -import type { DatasetCore } from "@rdfjs/types"; import type { Iri, VerifiableCredential, + VerifiableCredentialBase, VerifiablePresentation, } from "../common/common"; import { @@ -41,7 +41,7 @@ export type QueryByExample = { credentialQuery: { required?: boolean; reason?: string; - example: Partial & { + example: Partial & { credentialSchema?: { id: string; type: string; @@ -68,7 +68,7 @@ export type VerifiablePresentationRequest = { }; interface ParsedVerifiablePresentation extends VerifiablePresentation { - verifiableCredential?: (VerifiableCredential & DatasetCore)[]; + verifiableCredential?: VerifiableCredential[]; } /** diff --git a/src/verify/verify.ts b/src/verify/verify.ts index 0fb17342..c1a1bb66 100644 --- a/src/verify/verify.ts +++ b/src/verify/verify.ts @@ -27,7 +27,7 @@ import type { UrlString } from "@inrupt/solid-client"; import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; import type { - VerifiableCredential, + VerifiableCredentialBase, VerifiablePresentation, } from "../common/common"; import { @@ -39,12 +39,12 @@ import { import type { ParseOptions } from "../parser/jsonld"; async function dereferenceVc( - vc: VerifiableCredential | URL | UrlString, + vc: VerifiableCredentialBase | URL | UrlString, options?: ParseOptions, -): Promise { +): Promise { // This test passes for both URL and UrlString if (!vc.toString().startsWith("http")) { - return vc as VerifiableCredential; + return vc as VerifiableCredentialBase; } return getVerifiableCredential(vc.toString(), options); } @@ -68,7 +68,7 @@ async function dereferenceVc( * @since 0.3.0 */ export async function isValidVc( - vc: VerifiableCredential | URL | UrlString, + vc: VerifiableCredentialBase | URL | UrlString, options?: Partial<{ fetch?: typeof fetch; verificationEndpoint?: UrlString; From fcc22e8c372a0f86e94a3a9b981b5c6b6374e230 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 13 Nov 2023 23:20:51 +0000 Subject: [PATCH 25/79] chore: remove caching of odrl --- src/parser/contexts/index.ts | 3 - src/parser/contexts/odrl.ts | 222 ----------------------------------- 2 files changed, 225 deletions(-) delete mode 100644 src/parser/contexts/odrl.ts diff --git a/src/parser/contexts/index.ts b/src/parser/contexts/index.ts index afe51c16..384f8ded 100644 --- a/src/parser/contexts/index.ts +++ b/src/parser/contexts/index.ts @@ -25,7 +25,6 @@ import integrity from "./data-integrity"; import ed25519 from "./ed25519-2020"; import revocation from "./revocation-list"; import statusList from "./status-list"; -import odrl from "./odrl"; const contextDefinitions = { "https://www.w3.org/2018/credentials/v1": VC, @@ -38,8 +37,6 @@ export const cachedContexts = { "https://w3id.org/vc-revocation-list-2020/v1": revocation, "https://w3id.org/vc/status-list/2021/v1": statusList, "https://w3id.org/security/suites/ed25519-2020/v1": ed25519, - // FIXME: Double check if we need this - "http://www.w3.org/ns/odrl.jsonld": odrl, }; export const context = Object.keys(contextDefinitions); diff --git a/src/parser/contexts/odrl.ts b/src/parser/contexts/odrl.ts deleted file mode 100644 index a236c00a..00000000 --- a/src/parser/contexts/odrl.ts +++ /dev/null @@ -1,222 +0,0 @@ -// -// Copyright Inrupt Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -// Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -export default { - "@context": { - odrl: "http://www.w3.org/ns/odrl/2/", - rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - rdfs: "http://www.w3.org/2000/01/rdf-schema#", - owl: "http://www.w3.org/2002/07/owl#", - skos: "http://www.w3.org/2004/02/skos/core#", - dct: "http://purl.org/dc/terms/", - xsd: "http://www.w3.org/2001/XMLSchema#", - vcard: "http://www.w3.org/2006/vcard/ns#", - foaf: "http://xmlns.com/foaf/0.1/", - schema: "http://schema.org/", - cc: "http://creativecommons.org/ns#", - - uid: "@id", - type: "@type", - - Policy: "odrl:Policy", - Rule: "odrl:Rule", - profile: { "@type": "@id", "@id": "odrl:profile" }, - - inheritFrom: { "@type": "@id", "@id": "odrl:inheritFrom" }, - - ConflictTerm: "odrl:ConflictTerm", - conflict: { "@type": "@vocab", "@id": "odrl:conflict" }, - perm: "odrl:perm", - prohibit: "odrl:prohibit", - invalid: "odrl:invalid", - - Agreement: "odrl:Agreement", - Assertion: "odrl:Assertion", - Offer: "odrl:Offer", - Privacy: "odrl:Privacy", - Request: "odrl:Request", - Set: "odrl:Set", - Ticket: "odrl:Ticket", - - Asset: "odrl:Asset", - AssetCollection: "odrl:AssetCollection", - relation: { "@type": "@id", "@id": "odrl:relation" }, - hasPolicy: { "@type": "@id", "@id": "odrl:hasPolicy" }, - - target: { "@type": "@id", "@id": "odrl:target" }, - output: { "@type": "@id", "@id": "odrl:output" }, - - partOf: { "@type": "@id", "@id": "odrl:partOf" }, - source: { "@type": "@id", "@id": "odrl:source" }, - - Party: "odrl:Party", - PartyCollection: "odrl:PartyCollection", - function: { "@type": "@vocab", "@id": "odrl:function" }, - PartyScope: "odrl:PartyScope", - - assignee: { "@type": "@id", "@id": "odrl:assignee" }, - assigner: { "@type": "@id", "@id": "odrl:assigner" }, - assigneeOf: { "@type": "@id", "@id": "odrl:assigneeOf" }, - assignerOf: { "@type": "@id", "@id": "odrl:assignerOf" }, - attributedParty: { "@type": "@id", "@id": "odrl:attributedParty" }, - attributingParty: { "@type": "@id", "@id": "odrl:attributingParty" }, - compensatedParty: { "@type": "@id", "@id": "odrl:compensatedParty" }, - compensatingParty: { "@type": "@id", "@id": "odrl:compensatingParty" }, - consentingParty: { "@type": "@id", "@id": "odrl:consentingParty" }, - consentedParty: { "@type": "@id", "@id": "odrl:consentedParty" }, - informedParty: { "@type": "@id", "@id": "odrl:informedParty" }, - informingParty: { "@type": "@id", "@id": "odrl:informingParty" }, - trackingParty: { "@type": "@id", "@id": "odrl:trackingParty" }, - trackedParty: { "@type": "@id", "@id": "odrl:trackedParty" }, - contractingParty: { "@type": "@id", "@id": "odrl:contractingParty" }, - contractedParty: { "@type": "@id", "@id": "odrl:contractedParty" }, - - Action: "odrl:Action", - action: { "@type": "@vocab", "@id": "odrl:action" }, - includedIn: { "@type": "@id", "@id": "odrl:includedIn" }, - implies: { "@type": "@id", "@id": "odrl:implies" }, - - Permission: "odrl:Permission", - permission: { "@type": "@id", "@id": "odrl:permission" }, - - Prohibition: "odrl:Prohibition", - prohibition: { "@type": "@id", "@id": "odrl:prohibition" }, - - obligation: { "@type": "@id", "@id": "odrl:obligation" }, - - use: "odrl:use", - grantUse: "odrl:grantUse", - aggregate: "odrl:aggregate", - annotate: "odrl:annotate", - anonymize: "odrl:anonymize", - archive: "odrl:archive", - concurrentUse: "odrl:concurrentUse", - derive: "odrl:derive", - digitize: "odrl:digitize", - display: "odrl:display", - distribute: "odrl:distribute", - execute: "odrl:execute", - extract: "odrl:extract", - give: "odrl:give", - index: "odrl:index", - install: "odrl:install", - modify: "odrl:modify", - move: "odrl:move", - play: "odrl:play", - present: "odrl:present", - print: "odrl:print", - read: "odrl:read", - reproduce: "odrl:reproduce", - sell: "odrl:sell", - stream: "odrl:stream", - textToSpeech: "odrl:textToSpeech", - transfer: "odrl:transfer", - transform: "odrl:transform", - translate: "odrl:translate", - - Duty: "odrl:Duty", - duty: { "@type": "@id", "@id": "odrl:duty" }, - consequence: { "@type": "@id", "@id": "odrl:consequence" }, - remedy: { "@type": "@id", "@id": "odrl:remedy" }, - - acceptTracking: "odrl:acceptTracking", - attribute: "odrl:attribute", - compensate: "odrl:compensate", - delete: "odrl:delete", - ensureExclusivity: "odrl:ensureExclusivity", - include: "odrl:include", - inform: "odrl:inform", - nextPolicy: "odrl:nextPolicy", - obtainConsent: "odrl:obtainConsent", - reviewPolicy: "odrl:reviewPolicy", - uninstall: "odrl:uninstall", - watermark: "odrl:watermark", - - Constraint: "odrl:Constraint", - LogicalConstraint: "odrl:LogicalConstraint", - constraint: { "@type": "@id", "@id": "odrl:constraint" }, - refinement: { "@type": "@id", "@id": "odrl:refinement" }, - Operator: "odrl:Operator", - operator: { "@type": "@vocab", "@id": "odrl:operator" }, - RightOperand: "odrl:RightOperand", - rightOperand: "odrl:rightOperand", - rightOperandReference: { - "@type": "xsd:anyURI", - "@id": "odrl:rightOperandReference", - }, - LeftOperand: "odrl:LeftOperand", - leftOperand: { "@type": "@vocab", "@id": "odrl:leftOperand" }, - unit: "odrl:unit", - dataType: { "@type": "xsd:anyType", "@id": "odrl:datatype" }, - status: "odrl:status", - - absolutePosition: "odrl:absolutePosition", - absoluteSpatialPosition: "odrl:absoluteSpatialPosition", - absoluteTemporalPosition: "odrl:absoluteTemporalPosition", - absoluteSize: "odrl:absoluteSize", - count: "odrl:count", - dateTime: "odrl:dateTime", - delayPeriod: "odrl:delayPeriod", - deliveryChannel: "odrl:deliveryChannel", - elapsedTime: "odrl:elapsedTime", - event: "odrl:event", - fileFormat: "odrl:fileFormat", - industry: "odrl:industry:", - language: "odrl:language", - media: "odrl:media", - meteredTime: "odrl:meteredTime", - payAmount: "odrl:payAmount", - percentage: "odrl:percentage", - product: "odrl:product", - purpose: "odrl:purpose", - recipient: "odrl:recipient", - relativePosition: "odrl:relativePosition", - relativeSpatialPosition: "odrl:relativeSpatialPosition", - relativeTemporalPosition: "odrl:relativeTemporalPosition", - relativeSize: "odrl:relativeSize", - resolution: "odrl:resolution", - spatial: "odrl:spatial", - spatialCoordinates: "odrl:spatialCoordinates", - systemDevice: "odrl:systemDevice", - timeInterval: "odrl:timeInterval", - unitOfCount: "odrl:unitOfCount", - version: "odrl:version", - virtualLocation: "odrl:virtualLocation", - - eq: "odrl:eq", - gt: "odrl:gt", - gteq: "odrl:gteq", - lt: "odrl:lt", - lteq: "odrl:lteq", - neq: "odrl:neg", - isA: "odrl:isA", - hasPart: "odrl:hasPart", - isPartOf: "odrl:isPartOf", - isAllOf: "odrl:isAllOf", - isAnyOf: "odrl:isAnyOf", - isNoneOf: "odrl:isNoneOf", - or: "odrl:or", - xone: "odrl:xone", - and: "odrl:and", - andSequence: "odrl:andSequence", - - policyUsage: "odrl:policyUsage", - }, -}; From f97ed72ac175e6455c818d1a401d8254dd69a823 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 13 Nov 2023 23:26:25 +0000 Subject: [PATCH 26/79] chore: fix lint errors --- src/lookup/query.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lookup/query.test.ts b/src/lookup/query.test.ts index 2b5e88ee..c3580dc6 100644 --- a/src/lookup/query.test.ts +++ b/src/lookup/query.test.ts @@ -173,7 +173,6 @@ describe("query", () => { ); expect(vp).toMatchObject(mockDefaultPresentation()); expect(JSON.parse(JSON.stringify(vp))).toEqual(mockDefaultPresentation()); - }); it("normalizes the VP sent by the endpoint", async () => { From 74c0df4a033e5359a80af994e05baffd460a6971 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 13 Nov 2023 23:29:20 +0000 Subject: [PATCH 27/79] chore: fix type error in tests --- src/common/common.test.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 61195dc4..eb9bd415 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -362,7 +362,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow(/unsupported Content-Type/); }); @@ -378,7 +378,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", @@ -401,7 +401,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", @@ -432,7 +432,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", @@ -456,7 +456,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", @@ -474,7 +474,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", @@ -495,7 +495,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", @@ -519,7 +519,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", @@ -544,7 +544,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", @@ -566,7 +566,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", @@ -634,7 +634,7 @@ describe("getVerifiableCredential", () => { ); const vc = await getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }); // Since we have dataset properties in vc it should match the result @@ -660,7 +660,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", @@ -677,7 +677,7 @@ describe("getVerifiableCredential", () => { ); const vc = await getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }); const res = await jsonLdStringToStore( @@ -740,7 +740,7 @@ describe("getVerifiableCredential", () => { it("errors if the context contains an IRI that is not cached", async () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( "Unexpected context requested [http://example.org/my/sample/context]", @@ -761,7 +761,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], allowContextFetching: true, }), ).resolves.toMatchObject(mockCredential); @@ -770,7 +770,7 @@ describe("getVerifiableCredential", () => { it("resolves if the context is cached", async () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], allowContextFetching: true, contexts: { "http://example.org/my/sample/context": { From 54463008fc60700dbdb23a22c6a288741c5a6f9a Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 21 Nov 2023 01:32:04 +0000 Subject: [PATCH 28/79] chore: freeze response of verifiableCredentialToDataset --- src/common/common.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index 1cf6fa9d..9ac4d86f 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -420,7 +420,7 @@ export async function verifiableCredentialToDataset( ); } - return { + return Object.freeze({ ...vc, // Make this a DatasetCore without polluting the object with // all of the properties present in the N3.Store @@ -449,7 +449,7 @@ export async function verifiableCredentialToDataset( toJSON() { return vc; }, - }; + }); } /** From 3f798bdec8c62aceae377601b3e9e5ca17ebdb79 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 22 Nov 2023 01:11:14 +0000 Subject: [PATCH 29/79] WIP: RDFJS checkers --- package-lock.json | 8 +- package.json | 3 +- src/common/common.mock.ts | 4 + src/common/common.test.ts | 66 +++++++++- src/common/common.ts | 10 +- src/common/getters.ts | 267 ++++++++++++++++++++++++++++++++++++++ src/common/rdfjs.ts | 107 +++++++++++++++ 7 files changed, 452 insertions(+), 13 deletions(-) create mode 100644 src/common/getters.ts create mode 100644 src/common/rdfjs.ts diff --git a/package-lock.json b/package-lock.json index c561c8c5..00293332 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "jsonld-streaming-parser": "^3.3.0", "jsonld-streaming-serializer": "^2.1.0", "md5": "^2.3.0", - "n3": "^1.17.0" + "n3": "^1.17.0", + "rdf-namespaces": "^1.12.0" }, "devDependencies": { "@inrupt/eslint-config-lib": "^2.0.0", @@ -8027,6 +8028,11 @@ "@rdfjs/types": "*" } }, + "node_modules/rdf-namespaces": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/rdf-namespaces/-/rdf-namespaces-1.12.0.tgz", + "integrity": "sha512-Fk48ltssXTmyXeoLqC0y85CEAhhWH+wvu7bkr9WxsKUyFDcKwWSHOK7CvRq3XRampy1qhSrOsIQ8U1gQDCh5MA==" + }, "node_modules/rdf-string": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/rdf-string/-/rdf-string-1.6.3.tgz", diff --git a/package.json b/package.json index 749c3f83..908f6d1f 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,8 @@ "jsonld-streaming-parser": "^3.3.0", "jsonld-streaming-serializer": "^2.1.0", "md5": "^2.3.0", - "n3": "^1.17.0" + "n3": "^1.17.0", + "rdf-namespaces": "^1.12.0" }, "publishConfig": { "access": "public" diff --git a/src/common/common.mock.ts b/src/common/common.mock.ts index a3463a4d..6ee65c4c 100644 --- a/src/common/common.mock.ts +++ b/src/common/common.mock.ts @@ -141,6 +141,10 @@ export const mockPartialPresentation = ( claims?: Partial, ): Record => { return { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.inrupt.com/credentials/v1.jsonld", + ], id: claims?.id, type: claims?.type, verifiableCredential: credentials, diff --git a/src/common/common.test.ts b/src/common/common.test.ts index eb9bd415..7c979b52 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -32,7 +32,9 @@ import { isVerifiableCredential, isVerifiablePresentation, normalizeVc, + verifiableCredentialToDataset, } from "./common"; +import * as getters from "./getters"; import { defaultCredentialClaims, defaultVerifiableClaims, @@ -43,6 +45,8 @@ import { mockPartialPresentation, } from "./common.mock"; +const { namedNode } = DataFactory; + jest.mock("@inrupt/universal-fetch", () => { const fetchModule = jest.requireActual( "@inrupt/universal-fetch", @@ -63,8 +67,14 @@ describe("normalizeVc", () => { }); describe("isVerifiableCredential", () => { - it("returns true if all the expected fields are present in the credential", () => { + it("returns true if all the expected fields are present in the credential", async () => { expect(isVerifiableCredential(mockDefaultCredential())).toBe(true); + expect( + getters.isVerifiableCredential( + await verifiableCredentialToDataset(mockDefaultCredential()), + namedNode(mockDefaultCredential().id), + ), + ).toBe(true); }); describe("returns false if", () => { @@ -79,7 +89,20 @@ describe("isVerifiableCredential", () => { ["proofVerificationMethod"], ["proofPurpose"], ["proofValue"], - ])("is missing field %s", (entry) => { + ])("is missing field %s", async (entry) => { + expect( + getters.isVerifiableCredential( + await verifiableCredentialToDataset( + mockPartialCredential({ + ...defaultCredentialClaims, + [`${entry}`]: undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + ), + namedNode(mockDefaultCredential().id), + ), + ).toBe(false); + expect( isVerifiableCredential( mockPartialCredential({ @@ -90,13 +113,19 @@ describe("isVerifiableCredential", () => { ).toBe(false); }); - it("misses a credential subject", () => { + it("misses a credential subject", async () => { const mockedCredential = mockDefaultCredential(); delete ( mockedCredential as { credentialSubject: undefined | Record; } ).credentialSubject; + expect( + getters.isVerifiableCredential( + await verifiableCredentialToDataset(mockedCredential), + namedNode(mockDefaultCredential().id), + ), + ).toBe(false); expect(isVerifiableCredential(mockedCredential)).toBe(false); }); @@ -110,7 +139,19 @@ describe("isVerifiableCredential", () => { expect(isVerifiableCredential(mockedCredential)).toBe(false); }); - it("has an unexpected date format for the issuance", () => { + it("has an unexpected date format for the issuance", async () => { + expect( + getters.isVerifiableCredential( + await verifiableCredentialToDataset( + mockPartialCredential({ + ...defaultCredentialClaims, + issuanceDate: "Not a date", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + ), + namedNode(mockDefaultCredential().id), + ), + ).toBe(false); expect( isVerifiableCredential( mockPartialCredential({ @@ -121,7 +162,19 @@ describe("isVerifiableCredential", () => { ).toBe(false); }); - it("has an unexpected date format for the proof creation", () => { + it("has an unexpected date format for the proof creation", async () => { + expect( + getters.isVerifiableCredential( + await verifiableCredentialToDataset( + mockPartialCredential({ + ...defaultCredentialClaims, + proofCreated: "Not a date", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + ), + namedNode(mockDefaultCredential().id), + ), + ).toBe(false); expect( isVerifiableCredential( mockPartialCredential({ @@ -136,8 +189,9 @@ describe("isVerifiableCredential", () => { describe("isVerifiablePresentation", () => { describe("returns true", () => { - it("has all the expected fields are present in the credential", () => { + it("has all the expected fields are present in the credential", async () => { expect(isVerifiablePresentation(mockDefaultPresentation())).toBe(true); + // expect(getters.isVerifiablePresentation(await verifiableCredentialToDataset(mockDefaultPresentation()), namedNode(mockDefaultPresentation().id!))).toBe(true); }); it("has no associated credentials", () => { diff --git a/src/common/common.ts b/src/common/common.ts index 9ac4d86f..c261dbed 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -316,9 +316,9 @@ async function discoverLegacyEndpoints( }); // The dataset should have a single blank node subject of all its triples. - const wellKnownRootBlankNode = getThingAll(vcConfigData, { + const [wellKnownRootBlankNode] = getThingAll(vcConfigData, { acceptBlankNodes: true, - })[0]; + }); return { derivationService: @@ -407,10 +407,10 @@ export async function getVerifiableCredentialApiConfiguration( /** * @hidden */ -export async function verifiableCredentialToDataset( - vc: VerifiableCredentialBase, +export async function verifiableCredentialToDataset( + vc: T, options?: ParseOptions, -): Promise { +): Promise { let store: DatasetCore; try { store = await jsonLdToStore(vc, options); diff --git a/src/common/getters.ts b/src/common/getters.ts new file mode 100644 index 00000000..dd90bd33 --- /dev/null +++ b/src/common/getters.ts @@ -0,0 +1,267 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +import type { + Literal, + DatasetCore, + NamedNode, + Term, + BlankNode, +} from "@rdfjs/types"; +import { DataFactory } from "n3"; +import { rdf as _rdf } from "rdf-namespaces"; +import { getSingleObject } from "./rdfjs"; + +const { namedNode, defaultGraph, quad } = DataFactory; + +const SEC = "https://w3id.org/security#"; + +export const CRED = "https://www.w3.org/2018/credentials#"; +export const GC = "https://w3id.org/GConsent#"; +export const ACL = "http://www.w3.org/ns/auth/acl#"; +export const VC = "http://www.w3.org/ns/solid/vc#"; +export const XSD = "http://www.w3.org/2001/XMLSchema#"; +export const DC = "http://purl.org/dc/terms/"; + +export const XSD_BOOLEAN = namedNode(`${XSD}boolean`); + +export const SOLID_ACCESS_GRANT = namedNode(`${VC}SolidAccessGrant`); + +export const rdf = { + type: namedNode(_rdf.type), +}; + +export const xsd = { + boolean: namedNode(`${XSD}boolean`), + dateTime: namedNode(`${XSD}dateTime`), +}; + +export const cred = { + issuanceDate: namedNode(`${CRED}issuanceDate`), + expirationDate: namedNode(`${CRED}expirationDate`), + issuer: namedNode(`${CRED}issuer`), + credentialSubject: namedNode(`${CRED}credentialSubject`), + verifiableCredential: namedNode(`${CRED}verifiableCredential`), + holder: namedNode(`${CRED}holder`), + VerifiableCredential: namedNode(`${CRED}VerifiableCredential`), + VerifiablePresentation: namedNode(`${CRED}VerifiablePresentation`), +}; + +export const INHERIT = namedNode( + "urn:uuid:71ab2f68-a68b-4452-b968-dd23e0570227", +); + +const sec = { + proof: namedNode(`${SEC}proof`), + proofPurpose: namedNode(`${SEC}proofPurpose`), + proofValue: namedNode(`${SEC}proofValue`), + verificationMethod: namedNode(`${SEC}verificationMethod`), +}; + +export const dc = { + created: namedNode(`${DC}created`), +}; + +type VerifiableCredentialBase = DatasetCore & { id: string }; + +/** + * Get the ID (URL) of a Verifiable Credential. + * + * @example + * + * ``` + * const id = getId(vc); + * ``` + * + * @param vc The Verifiable Credential + * @returns The VC ID URL + */ +export function getId(vc: VerifiableCredentialBase): string { + return vc.id; +} + +/** + * @internal + */ +export function getCredentialSubject(vc: VerifiableCredentialBase) { + return getSingleObject( + vc, + namedNode(getId(vc)), + cred.credentialSubject, + "NamedNode", + ); +} + +/** + * Get the issuer of a Verifiable Credential. + * + * @example + * + * ``` + * const date = getIssuer(accessGrant); + * ``` + * + * @param vc The Access Grant/Request + * @returns The VC issuer + */ +export function getIssuer(vc: VerifiableCredentialBase): string { + return getSingleObject(vc, namedNode(getId(vc)), cred.issuer, "NamedNode") + .value; +} + +/** + * @internal + */ +function wrapDate(date: Literal) { + if ( + !date.datatype.equals( + namedNode("http://www.w3.org/2001/XMLSchema#dateTime"), + ) + ) { + throw new Error( + `Expected date to be a dateTime; recieved [${date.datatype.value}]`, + ); + } + return new Date(date.value); +} + +/** + * Get the issuance date of a Verifiable Credential. + * + * @example + * + * ``` + * const date = getIssuanceDate(accessGrant); + * ``` + * + * @param vc The Access Grant/Request + * @returns The issuance date + */ +export function getIssuanceDate(vc: VerifiableCredentialBase): Date { + return wrapDate( + getSingleObject(vc, namedNode(getId(vc)), cred.issuanceDate, "Literal"), + ); +} + +/** + * @internal + */ +function lenientSingle( + dataset: DatasetCore, + termTypes: T["termType"][] = ["NamedNode", "BlankNode"], +): T | undefined { + const array = [...dataset]; + return array.length === 1 && termTypes.includes(array[0].object.termType) + ? (array[0].object as T) + : undefined; +} + +/** + * @internal + */ +function isDate(literal?: Literal): boolean { + return ( + !!literal && + literal.datatype.equals(xsd.dateTime) && + !Number.isNaN(Date.parse(literal.value)) + ); +} + +function isValidProof( + dataset: DatasetCore, + proof: NamedNode | BlankNode, +): boolean { + return ( + isDate( + lenientSingle(dataset.match(null, dc.created, null, proof), [ + "Literal", + ]), + ) && + lenientSingle(dataset.match(null, sec.proofValue, null, proof), [ + "Literal", + ]) !== undefined && + lenientSingle( + dataset.match(null, sec.proofPurpose, null, proof), + ["NamedNode"], + ) !== undefined && + lenientSingle( + dataset.match(null, sec.verificationMethod, null, proof), + ["NamedNode"], + ) !== undefined && + lenientSingle(dataset.match(null, rdf.type, null, proof), [ + "NamedNode", + ]) !== undefined + ); +} + +export function isVerifiableCredential( + dataset: DatasetCore, + id: NamedNode, +): boolean { + const proof = lenientSingle( + dataset.match(id, sec.proof, null, defaultGraph()), + ); + return ( + !!proof && + isValidProof(dataset, proof) && + !!lenientSingle( + dataset.match(id, cred.issuer, null, defaultGraph()), + ["NamedNode"], + ) && + isDate( + lenientSingle( + dataset.match(id, cred.issuanceDate, null, defaultGraph()), + ["Literal"], + ), + ) && + !!lenientSingle( + dataset.match(id, cred.credentialSubject, null, defaultGraph()), + ["NamedNode"], + ) && + dataset.has(quad(id, rdf.type, cred.VerifiableCredential, defaultGraph())) + ); +} + +export function isVerifiablePresentation( + dataset: DatasetCore, + id: NamedNode, +): boolean { + for (const { object } of dataset.match( + id, + cred.verifiableCredential, + null, + defaultGraph(), + )) { + if ( + object.termType !== "NamedNode" || + !isVerifiableCredential(dataset, object) + ) { + return false; + } + } + + return ( + lenientSingle( + dataset.match(id, cred.holder, null, defaultGraph()), + ["NamedNode"], + ) !== undefined && + dataset.has(quad(id, rdf.type, cred.VerifiablePresentation, defaultGraph())) + ); +} diff --git a/src/common/rdfjs.ts b/src/common/rdfjs.ts new file mode 100644 index 00000000..930d5cdd --- /dev/null +++ b/src/common/rdfjs.ts @@ -0,0 +1,107 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +import type { + BlankNode, + DatasetCore, + Literal, + NamedNode, + Term, +} from "@rdfjs/types"; +import { DataFactory } from "n3"; + +const { defaultGraph } = DataFactory; + +/** + * @internal + */ +export function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "NamedNode", +): NamedNode; +export function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "NamedNode", + required: false, +): NamedNode | undefined; +export function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "Literal", + required: false, +): Literal | undefined; +export function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "BlankNode", +): BlankNode; +export function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: "Literal", +): Literal; +export function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, +): NamedNode | BlankNode; +export function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type: undefined, + required: false, +): NamedNode | BlankNode | undefined; +export function getSingleObject( + vc: DatasetCore, + subject: Term, + predicate: Term, + type?: Term["termType"], + required: "undefined-on-many" | boolean = true, +): Term | undefined { + const results = [...vc.match(subject, predicate, null, defaultGraph())]; + + if (results.length === 0 && !required) { + return undefined; + } + + if (results.length !== 1) { + throw new Error(`Expected exactly one result. Found ${results.length}.`); + } + + const [{ object }] = results; + const expectedTypes = type ? [type] : ["NamedNode", "BlankNode"]; + if (!expectedTypes.includes(object.termType)) { + throw new Error( + `Expected [${object.value}] to be a ${expectedTypes.join( + " or ", + )}. Found [${object.termType}]`, + ); + } + + return object; +} From 8e0df848733f4cb4cab66b5b47fc3118dc82c5cb Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 23 Nov 2023 15:31:32 +0000 Subject: [PATCH 30/79] chore: improve test coverage of getters --- src/common/constants.ts | 59 +++++++++++++ src/common/getters.test.ts | 166 +++++++++++++++++++++++++++++++++++++ src/common/getters.ts | 65 +++------------ 3 files changed, 235 insertions(+), 55 deletions(-) create mode 100644 src/common/constants.ts create mode 100644 src/common/getters.test.ts diff --git a/src/common/constants.ts b/src/common/constants.ts new file mode 100644 index 00000000..e2846ad3 --- /dev/null +++ b/src/common/constants.ts @@ -0,0 +1,59 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +import { DataFactory } from "n3"; +import { rdf as _rdf } from "rdf-namespaces"; + +const { namedNode } = DataFactory; + +const SEC = "https://w3id.org/security#"; +const CRED = "https://www.w3.org/2018/credentials#"; +const XSD = "http://www.w3.org/2001/XMLSchema#"; +const DC = "http://purl.org/dc/terms/"; + +export const rdf = { + type: namedNode(_rdf.type), +}; + +export const xsd = { + boolean: namedNode(`${XSD}boolean`), + dateTime: namedNode(`${XSD}dateTime`), +}; + +export const cred = { + issuanceDate: namedNode(`${CRED}issuanceDate`), + expirationDate: namedNode(`${CRED}expirationDate`), + issuer: namedNode(`${CRED}issuer`), + credentialSubject: namedNode(`${CRED}credentialSubject`), + verifiableCredential: namedNode(`${CRED}verifiableCredential`), + holder: namedNode(`${CRED}holder`), + VerifiableCredential: namedNode(`${CRED}VerifiableCredential`), + VerifiablePresentation: namedNode(`${CRED}VerifiablePresentation`), +}; +export const sec = { + proof: namedNode(`${SEC}proof`), + proofPurpose: namedNode(`${SEC}proofPurpose`), + proofValue: namedNode(`${SEC}proofValue`), + verificationMethod: namedNode(`${SEC}verificationMethod`), +}; + +export const dc = { + created: namedNode(`${DC}created`), +}; diff --git a/src/common/getters.test.ts b/src/common/getters.test.ts new file mode 100644 index 00000000..3b545d32 --- /dev/null +++ b/src/common/getters.test.ts @@ -0,0 +1,166 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { beforeAll, describe, expect, it } from "@jest/globals"; +import { Store, DataFactory } from "n3"; +import type { VerifiableCredential } from "./common"; +import { verifiableCredentialToDataset } from "./common"; +import { cred, xsd } from "./constants"; +import { mockDefaultCredential } from "./common.mock"; +import { + getCredentialSubject, + getId, + getIssuanceDate, + getIssuer, +} from "./getters"; + +const { quad, namedNode, blankNode, literal } = DataFactory; + +describe("getters", () => { + let defaultCredential: VerifiableCredential; + + beforeAll(async () => { + defaultCredential = await verifiableCredentialToDataset( + mockDefaultCredential(), + ); + }); + + it("getId", () => { + expect(getId(defaultCredential)).toBe(defaultCredential.id); + }); + + it("getIssuanceDate", () => { + expect(getIssuanceDate(defaultCredential)).toStrictEqual( + new Date(defaultCredential.issuanceDate), + ); + }); + + it("getIssuanceDate errors if issuance date is not a literal", () => { + const results = Object.assign( + new Store([ + ...[...defaultCredential].filter( + (quadTerm) => !quadTerm.predicate.equals(cred.issuanceDate), + ), + quad( + namedNode(defaultCredential.id), + cred.issuanceDate, + namedNode("http://example.org/not/a/date"), + ), + ]), + { id: defaultCredential.id }, + ); + + expect(() => getIssuanceDate(results)).toThrow( + "Expected [http://example.org/not/a/date] to be a Literal. Found [NamedNode]", + ); + }); + + it("getIssuanceDate errors if issuance date is not of type dateTime", () => { + const results = Object.assign( + new Store([ + ...[...defaultCredential].filter( + (quadTerm) => !quadTerm.predicate.equals(cred.issuanceDate), + ), + quad( + namedNode(defaultCredential.id), + cred.issuanceDate, + literal("not a dateTime"), + ), + ]), + { id: defaultCredential.id }, + ); + + expect(() => getIssuanceDate(results)).toThrow( + "Expected date to be a dateTime; recieved [http://www.w3.org/2001/XMLSchema#string]", + ); + }); + + it("getIssuanceDate errors if issuance date is a dateTime but has an invalid value for date", () => { + const results = Object.assign( + new Store([ + ...[...defaultCredential].filter( + (quadTerm) => !quadTerm.predicate.equals(cred.issuanceDate), + ), + quad( + namedNode(defaultCredential.id), + cred.issuanceDate, + literal("not a dateTime", xsd.dateTime), + ), + ]), + { id: defaultCredential.id }, + ); + + expect(() => + getIssuanceDate(results as unknown as VerifiableCredential), + ).toThrow("Found invalid value for date: [not a dateTime]"); + }); + + it("getIssuer", () => { + expect(getIssuer(defaultCredential)).toStrictEqual( + defaultCredential.issuer, + ); + }); + + it("getCredentialSubject", () => { + expect(getCredentialSubject(defaultCredential).value).toStrictEqual( + defaultCredential.credentialSubject.id, + ); + expect(getCredentialSubject(defaultCredential).termType).toBe("NamedNode"); + }); + + it("getCredentialSubject errors if there are multiple credential subjects", () => { + const results = Object.assign( + new Store([ + ...defaultCredential, + quad( + namedNode(defaultCredential.id), + cred.credentialSubject, + namedNode("http://example.org/my/second/subject"), + ), + ]), + { id: defaultCredential.id }, + ); + + expect(() => + getCredentialSubject(results as unknown as VerifiableCredential), + ).toThrow("Expected exactly one result. Found 2."); + }); + + it("getCredentialSubject errors if object is a Blank Node", () => { + const results = Object.assign( + new Store([ + ...[...defaultCredential].filter( + (quadTerm) => !quadTerm.predicate.equals(cred.credentialSubject), + ), + quad( + namedNode(defaultCredential.id), + cred.credentialSubject, + blankNode(), + ), + ]), + { id: defaultCredential.id }, + ); + + expect(() => + getCredentialSubject(results as unknown as VerifiableCredential), + ).toThrow("Expected [n3-0] to be a NamedNode. Found [BlankNode]"); + }); +}); diff --git a/src/common/getters.ts b/src/common/getters.ts index dd90bd33..4b5ecba6 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -26,60 +26,12 @@ import type { BlankNode, } from "@rdfjs/types"; import { DataFactory } from "n3"; -import { rdf as _rdf } from "rdf-namespaces"; import { getSingleObject } from "./rdfjs"; +import { cred, xsd, dc, sec, rdf } from "./constants"; const { namedNode, defaultGraph, quad } = DataFactory; -const SEC = "https://w3id.org/security#"; - -export const CRED = "https://www.w3.org/2018/credentials#"; -export const GC = "https://w3id.org/GConsent#"; -export const ACL = "http://www.w3.org/ns/auth/acl#"; -export const VC = "http://www.w3.org/ns/solid/vc#"; -export const XSD = "http://www.w3.org/2001/XMLSchema#"; -export const DC = "http://purl.org/dc/terms/"; - -export const XSD_BOOLEAN = namedNode(`${XSD}boolean`); - -export const SOLID_ACCESS_GRANT = namedNode(`${VC}SolidAccessGrant`); - -export const rdf = { - type: namedNode(_rdf.type), -}; - -export const xsd = { - boolean: namedNode(`${XSD}boolean`), - dateTime: namedNode(`${XSD}dateTime`), -}; - -export const cred = { - issuanceDate: namedNode(`${CRED}issuanceDate`), - expirationDate: namedNode(`${CRED}expirationDate`), - issuer: namedNode(`${CRED}issuer`), - credentialSubject: namedNode(`${CRED}credentialSubject`), - verifiableCredential: namedNode(`${CRED}verifiableCredential`), - holder: namedNode(`${CRED}holder`), - VerifiableCredential: namedNode(`${CRED}VerifiableCredential`), - VerifiablePresentation: namedNode(`${CRED}VerifiablePresentation`), -}; - -export const INHERIT = namedNode( - "urn:uuid:71ab2f68-a68b-4452-b968-dd23e0570227", -); - -const sec = { - proof: namedNode(`${SEC}proof`), - proofPurpose: namedNode(`${SEC}proofPurpose`), - proofValue: namedNode(`${SEC}proofValue`), - verificationMethod: namedNode(`${SEC}verificationMethod`), -}; - -export const dc = { - created: namedNode(`${DC}created`), -}; - -type VerifiableCredentialBase = DatasetCore & { id: string }; +export type DatasetWithId = DatasetCore & { id: string }; /** * Get the ID (URL) of a Verifiable Credential. @@ -93,14 +45,14 @@ type VerifiableCredentialBase = DatasetCore & { id: string }; * @param vc The Verifiable Credential * @returns The VC ID URL */ -export function getId(vc: VerifiableCredentialBase): string { +export function getId(vc: DatasetWithId): string { return vc.id; } /** * @internal */ -export function getCredentialSubject(vc: VerifiableCredentialBase) { +export function getCredentialSubject(vc: DatasetWithId) { return getSingleObject( vc, namedNode(getId(vc)), @@ -121,7 +73,7 @@ export function getCredentialSubject(vc: VerifiableCredentialBase) { * @param vc The Access Grant/Request * @returns The VC issuer */ -export function getIssuer(vc: VerifiableCredentialBase): string { +export function getIssuer(vc: DatasetWithId): string { return getSingleObject(vc, namedNode(getId(vc)), cred.issuer, "NamedNode") .value; } @@ -139,6 +91,9 @@ function wrapDate(date: Literal) { `Expected date to be a dateTime; recieved [${date.datatype.value}]`, ); } + if (Number.isNaN(Date.parse(date.value))) { + throw new Error(`Found invalid value for date: [${date.value}]`); + } return new Date(date.value); } @@ -151,10 +106,10 @@ function wrapDate(date: Literal) { * const date = getIssuanceDate(accessGrant); * ``` * - * @param vc The Access Grant/Request + * @param vc The Verifiable Credential * @returns The issuance date */ -export function getIssuanceDate(vc: VerifiableCredentialBase): Date { +export function getIssuanceDate(vc: DatasetWithId): Date { return wrapDate( getSingleObject(vc, namedNode(getId(vc)), cred.issuanceDate, "Literal"), ); From 7195c003e20d8e0dba5205c35994ddb934b82e3d Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:07:17 +0000 Subject: [PATCH 31/79] WIP: testing isVerifiablePresentation --- src/common/common.test.ts | 167 ++++++++++++++++++++++++-------------- src/common/common.ts | 2 +- src/common/getters.ts | 18 ++-- 3 files changed, 119 insertions(+), 68 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 7c979b52..94852aec 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -191,79 +191,106 @@ describe("isVerifiablePresentation", () => { describe("returns true", () => { it("has all the expected fields are present in the credential", async () => { expect(isVerifiablePresentation(mockDefaultPresentation())).toBe(true); - // expect(getters.isVerifiablePresentation(await verifiableCredentialToDataset(mockDefaultPresentation()), namedNode(mockDefaultPresentation().id!))).toBe(true); + expect( + getters.isVerifiablePresentation( + await verifiableCredentialToDataset(mockDefaultPresentation()), + namedNode(mockDefaultPresentation().id!), + ), + ).toBe(true); }); - it("has no associated credentials", () => { + it("has no associated credentials", async () => { expect(isVerifiablePresentation(mockDefaultPresentation([]))).toBe(true); + expect( + getters.isVerifiablePresentation( + await verifiableCredentialToDataset(mockDefaultPresentation([])), + namedNode(mockDefaultPresentation([]).id!), + ), + ).toBe(true); }); - it("has an URL shaped holder", () => { + it("has an URL shaped holder", async () => { const mockedPresentation = mockDefaultPresentation(); mockedPresentation.holder = "https://some.holder"; expect(isVerifiablePresentation(mockedPresentation)).toBe(true); + expect( + getters.isVerifiablePresentation( + await verifiableCredentialToDataset(mockedPresentation), + namedNode(mockedPresentation.id!), + ), + ).toBe(true); }); - it("is passed a correct VP with a single type", () => { - const vp = JSON.parse(`{ - "@context": [ - "https://www.w3.org/2018/credentials/v1" - ], - "holder": "https://vc.dev-next.inrupt.com", - "type": "VerifiablePresentation", - "verifiableCredential": [ - { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/security/suites/ed25519-2020/v1", - "https://w3id.org/vc-revocation-list-2020/v1", - "https://consent.pod.inrupt.com/credentials/v1" + it("is passed a correct VP with a single type", async () => { + const vp = { + "@context": ["https://www.w3.org/2018/credentials/v1"], + id: "https://example.org/ns/someCredentialInstance", + holder: "https://vc.dev-next.inrupt.com", + type: "VerifiablePresentation", + verifiableCredential: [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://consent.pod.inrupt.com/credentials/v1", + ], + credentialStatus: { + id: "https://vc.dev-next.inrupt.com/status/niiL#0", + revocationListCredential: + "https://vc.dev-next.inrupt.com/status/niiL", + revocationListIndex: "0", + type: "RevocationList2020Status", + }, + credentialSubject: { + providedConsent: { + mode: ["http://www.w3.org/ns/auth/acl#Read"], + hasStatus: + "https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", + forPersonalData: [ + "https://storage.dev-next.inrupt.com/8c6a313e-98ae-4eb2-9ab3-2df201d81a02/bookmarks/", ], - "credentialStatus": { - "id": "https://vc.dev-next.inrupt.com/status/niiL#0", - "revocationListCredential": "https://vc.dev-next.inrupt.com/status/niiL", - "revocationListIndex": "0", - "type": "RevocationList2020Status" - }, - "credentialSubject": { - "providedConsent": { - "mode": [ - "http://www.w3.org/ns/auth/acl#Read" - ], - "hasStatus": "https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", - "forPersonalData": [ - "https://storage.dev-next.inrupt.com/8c6a313e-98ae-4eb2-9ab3-2df201d81a02/bookmarks/" - ], - "forPurpose": "https://example.org/someSpecificPurpose", - "isProvidedTo": "https://pod.inrupt.com/womenofsolid/profile/card#me" - }, - "id": "https://id.dev-next.inrupt.com/virginia", - "inbox": "https://pod.inrupt.com/womenofsolid/inbox/" - }, - "id": "https://vc.dev-next.inrupt.com/vc/9f0855b1-7494-4770-8c49-c3fe91d82f93", - "issuanceDate": "2021-10-20T07:29:20.062Z", - "issuer": "https://vc.dev-next.inrupt.com", - "proof": { - "created": "2021-10-20T07:31:08.898Z", - "domain": "solid", - "proofPurpose": "assertionMethod", - "proofValue": "Q4-x1J0RqnIYBsW-O4IPskIeN_SOyUqtO8nZQdHlvz-PTwAe-L5lv2QhQHdUZpel1pEfdnll1rRD0vBdJ_svBg", - "type": "Ed25519Signature2020", - "verificationMethod": "https://vc.dev-next.inrupt.com/key/3eb16a3d-d31e-4f6e-b1ca-581257c69412" - }, - "type": [ - "VerifiableCredential", - "SolidAccessGrant" - ] - } - ] - }`); + forPurpose: "https://example.org/someSpecificPurpose", + isProvidedTo: + "https://pod.inrupt.com/womenofsolid/profile/card#me", + }, + id: "https://id.dev-next.inrupt.com/virginia", + inbox: "https://pod.inrupt.com/womenofsolid/inbox/", + }, + id: "https://vc.dev-next.inrupt.com/vc/9f0855b1-7494-4770-8c49-c3fe91d82f93", + issuanceDate: "2021-10-20T07:29:20.062Z", + issuer: "https://vc.dev-next.inrupt.com", + proof: { + created: "2021-10-20T07:31:08.898Z", + domain: "solid", + proofPurpose: "assertionMethod", + proofValue: + "Q4-x1J0RqnIYBsW-O4IPskIeN_SOyUqtO8nZQdHlvz-PTwAe-L5lv2QhQHdUZpel1pEfdnll1rRD0vBdJ_svBg", + type: "Ed25519Signature2020", + verificationMethod: + "https://vc.dev-next.inrupt.com/key/3eb16a3d-d31e-4f6e-b1ca-581257c69412", + }, + type: ["VerifiableCredential", "SolidAccessGrant"], + }, + ], + }; expect(isVerifiablePresentation(vp)).toBe(true); + expect( + getters.isVerifiablePresentation( + await verifiableCredentialToDataset(vp), + namedNode(vp.id), + ), + ).toBe(true); }); }); describe("returns false if", () => { - it.each([["type"]])("is missing field %s", (entry) => { + it.each([["type"]])("is missing field %s", async (entry) => { + const vp = mockPartialPresentation([], { + ...defaultVerifiableClaims, + [`${entry}`]: undefined, + }); + expect( isVerifiablePresentation( mockPartialPresentation([], { @@ -272,19 +299,41 @@ describe("isVerifiablePresentation", () => { }), ), ).toBe(false); + expect( + getters.isVerifiablePresentation( + // we expect that this is not a valid Verifiable Presentation + // @ts-expect-error + await verifiableCredentialToDataset(vp), + // we expect that this is not a valid Verifiable Presentation + // @ts-expect-error + namedNode(vp.id), + ), + ).toBe(false); }); - it("has a malformed credential", () => { + it("has a malformed credential", async () => { const mockedPresentation = mockDefaultPresentation([ {} as VerifiableCredential, ]); expect(isVerifiablePresentation(mockedPresentation)).toBe(false); + expect( + getters.isVerifiablePresentation( + await verifiableCredentialToDataset(mockedPresentation), + namedNode(mockedPresentation.id!), + ), + ).toBe(false); }); - it("has a non-URL shaped holder", () => { + it("has a non-URL shaped holder", async () => { const mockedPresentation = mockDefaultPresentation(); mockedPresentation.holder = "some non-URL holder"; expect(isVerifiablePresentation(mockedPresentation)).toBe(false); + expect( + getters.isVerifiablePresentation( + await verifiableCredentialToDataset(mockedPresentation), + namedNode(mockedPresentation.id!), + ), + ).toBe(false); }); }); }); diff --git a/src/common/common.ts b/src/common/common.ts index c261dbed..7b570748 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -202,7 +202,7 @@ export function isVerifiableCredential( return dataIsVc; } -function isUrl(url: string): boolean { +export function isUrl(url: string): boolean { try { // If url is not URL-shaped, this will throw. // eslint-disable-next-line no-new diff --git a/src/common/getters.ts b/src/common/getters.ts index 4b5ecba6..97be59f0 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -28,6 +28,7 @@ import type { import { DataFactory } from "n3"; import { getSingleObject } from "./rdfjs"; import { cred, xsd, dc, sec, rdf } from "./constants"; +import { isUrl } from "./common"; const { namedNode, defaultGraph, quad } = DataFactory; @@ -67,10 +68,10 @@ export function getCredentialSubject(vc: DatasetWithId) { * @example * * ``` - * const date = getIssuer(accessGrant); + * const date = getIssuer(vc); * ``` * - * @param vc The Access Grant/Request + * @param vc The Verifiable Credential * @returns The VC issuer */ export function getIssuer(vc: DatasetWithId): string { @@ -103,7 +104,7 @@ function wrapDate(date: Literal) { * @example * * ``` - * const date = getIssuanceDate(accessGrant); + * const date = getIssuanceDate(vc); * ``` * * @param vc The Verifiable Credential @@ -212,11 +213,12 @@ export function isVerifiablePresentation( } } + const holder = [...dataset.match(id, cred.holder, null, defaultGraph())]; return ( - lenientSingle( - dataset.match(id, cred.holder, null, defaultGraph()), - ["NamedNode"], - ) !== undefined && - dataset.has(quad(id, rdf.type, cred.VerifiablePresentation, defaultGraph())) + (holder.length === 0 || + (holder.length === 1 && holder[0].object.termType === "NamedNode" && isUrl(holder[0].object.value))) && + // dataset.has(quad(id, rdf.type, cred.VerifiablePresentation, defaultGraph())) + // FIXME: Replace with the above condition + dataset.match(id, rdf.type, null, defaultGraph()).size >= 1 ); } From b954cb0a2913e65d3131e1d19bedb9e913eea67b Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 23 Nov 2023 20:54:27 +0000 Subject: [PATCH 32/79] chore: fix isVerifiablePresentation RDFJS test --- src/common/common.test.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 94852aec..5ae93ddd 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -44,6 +44,7 @@ import { mockPartialCredential, mockPartialPresentation, } from "./common.mock"; +import { cred } from "./constants"; const { namedNode } = DataFactory; @@ -311,29 +312,35 @@ describe("isVerifiablePresentation", () => { ).toBe(false); }); - it("has a malformed credential", async () => { + it("has a malformed credential that is not parsed into json-ld", async () => { const mockedPresentation = mockDefaultPresentation([ {} as VerifiableCredential, ]); + + const mockedPresentationAsDataset = await verifiableCredentialToDataset(mockedPresentation); + expect(mockedPresentationAsDataset.match(null, cred.verifiableCredential, null).size).toEqual(0); expect(isVerifiablePresentation(mockedPresentation)).toBe(false); expect( getters.isVerifiablePresentation( - await verifiableCredentialToDataset(mockedPresentation), + mockedPresentationAsDataset, namedNode(mockedPresentation.id!), ), - ).toBe(false); + ).toBe(true); }); it("has a non-URL shaped holder", async () => { const mockedPresentation = mockDefaultPresentation(); mockedPresentation.holder = "some non-URL holder"; expect(isVerifiablePresentation(mockedPresentation)).toBe(false); + + const presentationAsDataset = await verifiableCredentialToDataset(mockedPresentation); + expect(presentationAsDataset.match(null, cred.holder, null).size).toEqual(0); expect( getters.isVerifiablePresentation( - await verifiableCredentialToDataset(mockedPresentation), + presentationAsDataset, namedNode(mockedPresentation.id!), ), - ).toBe(false); + ).toBe(true); }); }); }); From b9e7b90f6b6b5c895957f60cd253a4eea64a8766 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:25:25 +0000 Subject: [PATCH 33/79] chore: improve isVerifiablePresentation tests --- src/common/common.test.ts | 77 ++++++++++++++++++++++++++++++++++----- src/common/getters.ts | 4 +- src/common/rdfjs.ts | 39 +------------------- 3 files changed, 71 insertions(+), 49 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 5ae93ddd..874abb73 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -21,7 +21,7 @@ import type * as UniversalFetch from "@inrupt/universal-fetch"; import { Response, fetch as uniFetch } from "@inrupt/universal-fetch"; import { describe, expect, it, jest, beforeEach } from "@jest/globals"; -import { DataFactory } from "n3"; +import { DataFactory, Store } from "n3"; import { isomorphic } from "rdf-isomorphic"; import type { IJsonLdContext } from "jsonld-context-parser"; import { jsonLdStringToStore } from "../parser/jsonld"; @@ -46,7 +46,7 @@ import { } from "./common.mock"; import { cred } from "./constants"; -const { namedNode } = DataFactory; +const { namedNode, quad, blankNode } = DataFactory; jest.mock("@inrupt/universal-fetch", () => { const fetchModule = jest.requireActual( @@ -302,11 +302,9 @@ describe("isVerifiablePresentation", () => { ).toBe(false); expect( getters.isVerifiablePresentation( - // we expect that this is not a valid Verifiable Presentation - // @ts-expect-error + // @ts-expect-error we expect that this is not a valid Verifiable Presentation await verifiableCredentialToDataset(vp), - // we expect that this is not a valid Verifiable Presentation - // @ts-expect-error + // @ts-expect-error we expect that this is not a valid Verifiable Presentation namedNode(vp.id), ), ).toBe(false); @@ -317,8 +315,12 @@ describe("isVerifiablePresentation", () => { {} as VerifiableCredential, ]); - const mockedPresentationAsDataset = await verifiableCredentialToDataset(mockedPresentation); - expect(mockedPresentationAsDataset.match(null, cred.verifiableCredential, null).size).toEqual(0); + const mockedPresentationAsDataset = + await verifiableCredentialToDataset(mockedPresentation); + expect( + mockedPresentationAsDataset.match(null, cred.verifiableCredential, null) + .size, + ).toBe(0); expect(isVerifiablePresentation(mockedPresentation)).toBe(false); expect( getters.isVerifiablePresentation( @@ -326,6 +328,35 @@ describe("isVerifiablePresentation", () => { namedNode(mockedPresentation.id!), ), ).toBe(true); + + // Should return false when we artifically add a blank node to the dataset + expect( + getters.isVerifiablePresentation( + new Store([ + ...mockedPresentationAsDataset, + quad( + namedNode(mockedPresentation.id!), + cred.verifiableCredential, + blankNode(), + ), + ]), + namedNode(mockedPresentation.id!), + ), + ).toBe(false); + // Should return false when we artifically add a named node with invalid url to the dataset + expect( + getters.isVerifiablePresentation( + new Store([ + ...mockedPresentationAsDataset, + quad( + namedNode(mockedPresentation.id!), + cred.verifiableCredential, + namedNode("http://example.org/incomplete/vc"), + ), + ]), + namedNode(mockedPresentation.id!), + ), + ).toBe(false); }); it("has a non-URL shaped holder", async () => { @@ -333,14 +364,40 @@ describe("isVerifiablePresentation", () => { mockedPresentation.holder = "some non-URL holder"; expect(isVerifiablePresentation(mockedPresentation)).toBe(false); - const presentationAsDataset = await verifiableCredentialToDataset(mockedPresentation); - expect(presentationAsDataset.match(null, cred.holder, null).size).toEqual(0); + const presentationAsDataset = + await verifiableCredentialToDataset(mockedPresentation); + expect(presentationAsDataset.match(null, cred.holder, null).size).toBe(0); expect( getters.isVerifiablePresentation( presentationAsDataset, namedNode(mockedPresentation.id!), ), ).toBe(true); + + // Should return false when we artifically add a blank node to the dataset + expect( + getters.isVerifiablePresentation( + new Store([ + ...presentationAsDataset, + quad(namedNode(mockedPresentation.id!), cred.holder, blankNode()), + ]), + namedNode(mockedPresentation.id!), + ), + ).toBe(false); + // Should return false when we artifically add a named node with invalid url to the dataset + expect( + getters.isVerifiablePresentation( + new Store([ + ...presentationAsDataset, + quad( + namedNode(mockedPresentation.id!), + cred.holder, + namedNode("not a valid url"), + ), + ]), + namedNode(mockedPresentation.id!), + ), + ).toBe(false); }); }); }); diff --git a/src/common/getters.ts b/src/common/getters.ts index 97be59f0..938aa061 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -216,7 +216,9 @@ export function isVerifiablePresentation( const holder = [...dataset.match(id, cred.holder, null, defaultGraph())]; return ( (holder.length === 0 || - (holder.length === 1 && holder[0].object.termType === "NamedNode" && isUrl(holder[0].object.value))) && + (holder.length === 1 && + holder[0].object.termType === "NamedNode" && + isUrl(holder[0].object.value))) && // dataset.has(quad(id, rdf.type, cred.VerifiablePresentation, defaultGraph())) // FIXME: Replace with the above condition dataset.match(id, rdf.type, null, defaultGraph()).size >= 1 diff --git a/src/common/rdfjs.ts b/src/common/rdfjs.ts index 930d5cdd..937f43de 100644 --- a/src/common/rdfjs.ts +++ b/src/common/rdfjs.ts @@ -38,63 +38,26 @@ export function getSingleObject( predicate: Term, type: "NamedNode", ): NamedNode; -export function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type: "NamedNode", - required: false, -): NamedNode | undefined; -export function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type: "Literal", - required: false, -): Literal | undefined; -export function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type: "BlankNode", -): BlankNode; export function getSingleObject( vc: DatasetCore, subject: Term, predicate: Term, type: "Literal", ): Literal; -export function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, -): NamedNode | BlankNode; -export function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type: undefined, - required: false, -): NamedNode | BlankNode | undefined; export function getSingleObject( vc: DatasetCore, subject: Term, predicate: Term, type?: Term["termType"], - required: "undefined-on-many" | boolean = true, ): Term | undefined { const results = [...vc.match(subject, predicate, null, defaultGraph())]; - if (results.length === 0 && !required) { - return undefined; - } - if (results.length !== 1) { throw new Error(`Expected exactly one result. Found ${results.length}.`); } const [{ object }] = results; - const expectedTypes = type ? [type] : ["NamedNode", "BlankNode"]; + const expectedTypes = [type]; if (!expectedTypes.includes(object.termType)) { throw new Error( `Expected [${object.value}] to be a ${expectedTypes.join( From cc4dde59ac83e4442722cbc90cc50893ab426f15 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:28:38 +0000 Subject: [PATCH 34/79] chore: run lint:fix --- src/common/rdfjs.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/common/rdfjs.ts b/src/common/rdfjs.ts index 937f43de..005fc039 100644 --- a/src/common/rdfjs.ts +++ b/src/common/rdfjs.ts @@ -18,13 +18,7 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type { - BlankNode, - DatasetCore, - Literal, - NamedNode, - Term, -} from "@rdfjs/types"; +import type { DatasetCore, Literal, NamedNode, Term } from "@rdfjs/types"; import { DataFactory } from "n3"; const { defaultGraph } = DataFactory; From 08ad5f29427df8e4b3ef7a1d796d2d232c55861e Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:23:06 +0000 Subject: [PATCH 35/79] chore: export getCredentialSubject --- src/common/getters.ts | 34 +++++++++++++++++++++++++++++++++- src/index.ts | 6 ++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/common/getters.ts b/src/common/getters.ts index 938aa061..2317ba17 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -51,7 +51,16 @@ export function getId(vc: DatasetWithId): string { } /** - * @internal + * Get the subject of a Verifiable Credential. + * + * @example + * + * ``` + * const date = getCredentialSubject(vc); + * ``` + * + * @param vc The Verifiable Credential + * @returns The VC subject */ export function getCredentialSubject(vc: DatasetWithId) { return getSingleObject( @@ -116,6 +125,29 @@ export function getIssuanceDate(vc: DatasetWithId): Date { ); } +/** + * Get the expiration date of an Access Grant/Request. + * + * @example + * + * ``` + * const date = getExpirationDate(accessGrant); + * ``` + * + * @param vc The Access Grant/Request + * @returns The expiration date + */ +export function getExpirationDate(vc: DatasetWithId): Date | undefined { + const expirationDate = getLenin( + vc, + namedNode(getId(vc)), + cred.expirationDate, + "Literal", + false, + ); + return expirationDate && wrapDate(expirationDate); +} + /** * @internal */ diff --git a/src/index.ts b/src/index.ts index a7a7cadc..21303048 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,3 +39,9 @@ export type { } from "./lookup/query"; export { revokeVerifiableCredential } from "./revoke/revoke"; export { isValidVc, isValidVerifiablePresentation } from "./verify/verify"; +export { + getId, + getIssuanceDate, + getIssuer, + getCredentialSubject, +} from "./common/getters"; From eecca3254fee4ae97f673f494d68d5292f671ca2 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Fri, 24 Nov 2023 10:34:22 +0000 Subject: [PATCH 36/79] chore: export getExpirationDate and rdfjs verifiableCredential checkers --- src/common/common.ts | 4 +++ src/common/getters.test.ts | 72 ++++++++++++++++++++++++++++++++++++++ src/common/getters.ts | 55 +++++++++++++---------------- src/common/rdfjs.ts | 35 ++++++++++-------- src/index.test.ts | 27 ++++++++++++++ src/index.ts | 3 ++ 6 files changed, 151 insertions(+), 45 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index 7b570748..cccea8d7 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -157,6 +157,7 @@ export function normalizeVp(vpJson: T): T { * schema we expect. * @param data The JSON-LD payload * @returns true is the payload matches our expectation. + * @deprecated Use isRdfjsVerifiableCredential instead */ export function isVerifiableCredential( data: unknown | VerifiableCredentialBase, @@ -213,6 +214,9 @@ export function isUrl(url: string): boolean { } } +/** + * @deprecated Use isRdfjsVerifiableCredential instead + */ export function isVerifiablePresentation( vp: unknown | VerifiablePresentation, ): vp is VerifiablePresentation { diff --git a/src/common/getters.test.ts b/src/common/getters.test.ts index 3b545d32..7293a13a 100644 --- a/src/common/getters.test.ts +++ b/src/common/getters.test.ts @@ -27,6 +27,7 @@ import { cred, xsd } from "./constants"; import { mockDefaultCredential } from "./common.mock"; import { getCredentialSubject, + getExpirationDate, getId, getIssuanceDate, getIssuer, @@ -113,6 +114,77 @@ describe("getters", () => { ).toThrow("Found invalid value for date: [not a dateTime]"); }); + describe("getExpirationDate", () => { + it("returns undefined if there is no expiration date", () => { + expect(getExpirationDate(defaultCredential)).toBeUndefined(); + }); + + it("gets the access expiration date", async () => { + const expirationDate = new Date(Date.now()).toString(); + const credential = await verifiableCredentialToDataset({ + ...mockDefaultCredential(), + expirationDate, + }); + expect(getExpirationDate(credential)).toStrictEqual( + new Date(expirationDate), + ); + }); + + it("errors if the expiration date is a NamedNode", () => { + const store = Object.assign(new Store([...defaultCredential]), { + id: defaultCredential.id, + }); + + store.addQuad( + namedNode(getId(defaultCredential)), + cred.expirationDate, + namedNode("http://example.org/this/is/a/date"), + ); + + expect(() => getExpirationDate(store)).toThrow( + "Expected expiration date to be a Literal. Found [http://example.org/this/is/a/date] of type [NamedNode].", + ); + }); + + it("errors if there are multiple expiration dates", () => { + const store = Object.assign(new Store([...defaultCredential]), { + id: defaultCredential.id, + }); + + store.addQuad( + namedNode(getId(defaultCredential)), + cred.expirationDate, + literal(new Date(1700820377111).toString(), xsd.dateTime), + ); + + store.addQuad( + namedNode(getId(defaultCredential)), + cred.expirationDate, + literal(new Date(1700820300000).toString(), xsd.dateTime), + ); + + expect(() => getExpirationDate(store)).toThrow( + "Expected 0 or 1 expiration date. Found 2.", + ); + }); + + it("errors if the expiration date is a literal without xsd:type", async () => { + const store = Object.assign(new Store([...defaultCredential]), { + id: defaultCredential.id, + }); + + store.addQuad( + namedNode(getId(defaultCredential)), + cred.expirationDate, + literal("boo"), + ); + + expect(() => getExpirationDate(store)).toThrow( + "Expected date to be a dateTime; recieved [http://www.w3.org/2001/XMLSchema#string]", + ); + }); + }); + it("getIssuer", () => { expect(getIssuer(defaultCredential)).toStrictEqual( defaultCredential.issuer, diff --git a/src/common/getters.ts b/src/common/getters.ts index 2317ba17..a583d6e4 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -18,15 +18,9 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type { - Literal, - DatasetCore, - NamedNode, - Term, - BlankNode, -} from "@rdfjs/types"; +import type { Literal, DatasetCore, NamedNode, BlankNode } from "@rdfjs/types"; import { DataFactory } from "n3"; -import { getSingleObject } from "./rdfjs"; +import { getSingleObject, lenientSingle } from "./rdfjs"; import { cred, xsd, dc, sec, rdf } from "./constants"; import { isUrl } from "./common"; @@ -126,39 +120,38 @@ export function getIssuanceDate(vc: DatasetWithId): Date { } /** - * Get the expiration date of an Access Grant/Request. + * Get the expiration date of a Verifiable Credential. * * @example * * ``` - * const date = getExpirationDate(accessGrant); + * const date = getExpirationDate(vc); * ``` * - * @param vc The Access Grant/Request + * @param vc The Verifiable Credential * @returns The expiration date */ export function getExpirationDate(vc: DatasetWithId): Date | undefined { - const expirationDate = getLenin( - vc, - namedNode(getId(vc)), - cred.expirationDate, - "Literal", - false, - ); - return expirationDate && wrapDate(expirationDate); -} + const res = [ + ...vc.match( + namedNode(getId(vc)), + cred.expirationDate, + undefined, + defaultGraph(), + ), + ]; -/** - * @internal - */ -function lenientSingle( - dataset: DatasetCore, - termTypes: T["termType"][] = ["NamedNode", "BlankNode"], -): T | undefined { - const array = [...dataset]; - return array.length === 1 && termTypes.includes(array[0].object.termType) - ? (array[0].object as T) - : undefined; + if (res.length === 0) return undefined; + + if (res.length !== 1) + throw new Error(`Expected 0 or 1 expiration date. Found ${res.length}.`); + + if (res[0].object.termType !== "Literal") + throw new Error( + `Expected expiration date to be a Literal. Found [${res[0].object.value}] of type [${res[0].object.termType}].`, + ); + + return wrapDate(res[0].object); } /** diff --git a/src/common/rdfjs.ts b/src/common/rdfjs.ts index 005fc039..90d06600 100644 --- a/src/common/rdfjs.ts +++ b/src/common/rdfjs.ts @@ -18,7 +18,7 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type { DatasetCore, Literal, NamedNode, Term } from "@rdfjs/types"; +import type { DatasetCore, Term } from "@rdfjs/types"; import { DataFactory } from "n3"; const { defaultGraph } = DataFactory; @@ -26,24 +26,18 @@ const { defaultGraph } = DataFactory; /** * @internal */ -export function getSingleObject( +export function getSingleObject( vc: DatasetCore, subject: Term, predicate: Term, - type: "NamedNode", -): NamedNode; -export function getSingleObject( + type: Term["termType"], +): T; +export function getSingleObject( vc: DatasetCore, subject: Term, predicate: Term, - type: "Literal", -): Literal; -export function getSingleObject( - vc: DatasetCore, - subject: Term, - predicate: Term, - type?: Term["termType"], -): Term | undefined { + type?: T["termType"], +): T { const results = [...vc.match(subject, predicate, null, defaultGraph())]; if (results.length !== 1) { @@ -60,5 +54,18 @@ export function getSingleObject( ); } - return object; + return object as T; +} + +/** + * @internal + */ +export function lenientSingle( + dataset: DatasetCore, + termTypes: T["termType"][] = ["NamedNode", "BlankNode"], +): T | undefined { + const array = [...dataset]; + return array.length === 1 && termTypes.includes(array[0].object.termType) + ? (array[0].object as T) + : undefined; } diff --git a/src/index.test.ts b/src/index.test.ts index 5b18a034..f5031f1e 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -28,6 +28,15 @@ import { getVerifiableCredentialApiConfiguration, verifiableCredentialToDataset, } from "./common/common"; +import { + getId, + getCredentialSubject, + getExpirationDate, + getIssuanceDate, + getIssuer, + isVerifiableCredential as isRdfjsVerifiableCredential, + isVerifiablePresentation as isRdfjsVerifiablePresentation, +} from "./common/getters"; import getVerifiableCredentialAllFromShape from "./lookup/derive"; import revokeVerifiableCredential from "./revoke/revoke"; import { isValidVc, isValidVerifiablePresentation } from "./verify/verify"; @@ -47,6 +56,13 @@ describe("exports", () => { "revokeVerifiableCredential", "isValidVc", "isValidVerifiablePresentation", + "getId", + "getIssuanceDate", + "getIssuer", + "getCredentialSubject", + "getExpirationDate", + "isRdfjsVerifiableCredential", + "isRdfjsVerifiablePresentation", ]); expect(packageExports.issueVerifiableCredential).toBe( issueVerifiableCredential, @@ -75,5 +91,16 @@ describe("exports", () => { isValidVerifiablePresentation, ); expect(packageExports.query).toBe(query); + expect(packageExports.getId).toBe(getId); + expect(packageExports.getIssuanceDate).toBe(getIssuanceDate); + expect(packageExports.getIssuer).toBe(getIssuer); + expect(packageExports.getExpirationDate).toBe(getExpirationDate); + expect(packageExports.getCredentialSubject).toBe(getCredentialSubject); + expect(packageExports.isRdfjsVerifiableCredential).toBe( + isRdfjsVerifiableCredential, + ); + expect(packageExports.isRdfjsVerifiablePresentation).toBe( + isRdfjsVerifiablePresentation, + ); }); }); diff --git a/src/index.ts b/src/index.ts index 21303048..d4f53b4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,4 +44,7 @@ export { getIssuanceDate, getIssuer, getCredentialSubject, + getExpirationDate, + isVerifiableCredential as isRdfjsVerifiableCredential, + isVerifiablePresentation as isRdfjsVerifiablePresentation, } from "./common/getters"; From a377e6fb94741d25a9fe5ed31225c5b658e7d051 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Fri, 24 Nov 2023 10:42:24 +0000 Subject: [PATCH 37/79] chore: add links for cached contexts --- src/parser/contexts/data-integrity.ts | 4 ++++ src/parser/contexts/ed25519-2020.ts | 4 ++++ src/parser/contexts/inrupt-vc.ts | 4 ++++ src/parser/contexts/revocation-list.ts | 4 ++++ src/parser/contexts/status-list.ts | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/src/parser/contexts/data-integrity.ts b/src/parser/contexts/data-integrity.ts index c9898768..dabb64ba 100644 --- a/src/parser/contexts/data-integrity.ts +++ b/src/parser/contexts/data-integrity.ts @@ -18,6 +18,10 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // + +/** + * @see https://w3id.org/security/data-integrity/v1 + */ export default { "@context": { id: "@id", diff --git a/src/parser/contexts/ed25519-2020.ts b/src/parser/contexts/ed25519-2020.ts index b65eb596..6b55a9cb 100644 --- a/src/parser/contexts/ed25519-2020.ts +++ b/src/parser/contexts/ed25519-2020.ts @@ -18,6 +18,10 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // + +/** + * @see https://w3id.org/security/suites/ed25519-2020/v1 + */ export default { "@context": { id: "@id", diff --git a/src/parser/contexts/inrupt-vc.ts b/src/parser/contexts/inrupt-vc.ts index 56ed156c..cc3aa60b 100644 --- a/src/parser/contexts/inrupt-vc.ts +++ b/src/parser/contexts/inrupt-vc.ts @@ -18,6 +18,10 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // + +/** + * @see https://vc.inrupt.com/credentials/v1 + */ export default { "@context": { "@version": 1.1, diff --git a/src/parser/contexts/revocation-list.ts b/src/parser/contexts/revocation-list.ts index 08a7861b..037be293 100644 --- a/src/parser/contexts/revocation-list.ts +++ b/src/parser/contexts/revocation-list.ts @@ -18,6 +18,10 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // + +/** + * @see https://w3id.org/vc-revocation-list-2020/v1 + */ export default { "@context": { "@protected": true, diff --git a/src/parser/contexts/status-list.ts b/src/parser/contexts/status-list.ts index 24653e81..f7756e26 100644 --- a/src/parser/contexts/status-list.ts +++ b/src/parser/contexts/status-list.ts @@ -18,6 +18,10 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // + +/** + * @see https://w3id.org/vc/status-list/2021/v1 + */ export default { "@context": { "@protected": true, From cfdcf877e2f26c5e7bc7c83ea3dd8d258a03de9e Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Fri, 24 Nov 2023 14:48:08 +0000 Subject: [PATCH 38/79] fix: getCredentialSubject should always return a NamedNode --- src/common/getters.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/getters.ts b/src/common/getters.ts index a583d6e4..50285df4 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -56,8 +56,8 @@ export function getId(vc: DatasetWithId): string { * @param vc The Verifiable Credential * @returns The VC subject */ -export function getCredentialSubject(vc: DatasetWithId) { - return getSingleObject( +export function getCredentialSubject(vc: DatasetWithId): NamedNode { + return getSingleObject( vc, namedNode(getId(vc)), cred.credentialSubject, From 609a28bb3645a14fecdc11244eb0e49c74be9f31 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Fri, 24 Nov 2023 19:06:15 +0000 Subject: [PATCH 39/79] chore: allow 60 seconds for shape matching test --- e2e/node/e2e.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index caf34ee5..7a397e67 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -221,7 +221,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { }, ), ]); - }); + }, 60_000); }); describe("revoke VCs", () => { From c7f794946d2abff63db4c63e5b4072435c719c52 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 25 Nov 2023 11:07:00 +0000 Subject: [PATCH 40/79] chore: use vcConfiguration endpoints instead of hard coded ones --- e2e/node/e2e.test.ts | 58 +++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index 7a397e67..598f9c44 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -30,9 +30,11 @@ import { } from "@inrupt/internal-test-env"; import { getVerifiableCredentialAllFromShape, + getVerifiableCredentialApiConfiguration, issueVerifiableCredential, revokeVerifiableCredential, } from "../../src/index"; +import { VerifiableCredentialApiConfiguration } from "../../src/common/common"; const validCredentialClaims = { "@context": [ @@ -83,6 +85,11 @@ const env = getNodeTestingEnvironment({ describe("End-to-end verifiable credentials tests for environment", () => { let vcSubject: string; let session: Session; + let issuerService: string; + let derivationService: string; + let statusService: string; + let verifierService: string; + beforeEach(async () => { session = await getAuthenticatedSession(env); @@ -92,15 +99,22 @@ describe("End-to-end verifiable credentials tests for environment", () => { vcSubject = session.info.webId; } - // The following code snippet doesn't work in Jest, probably because of - // https://github.com/standard-things/esm/issues/706 which seems to be - // related to https://github.com/facebook/jest/issues/9430. The JSON-LD - // module depends on @digitalbazaar/http-client, which uses esm, which - // looks like it confuses Jest. Working around this by hard-coding the - // endpoints IRIs. - // vcConfiguration = await getVerifiableCredentialApiConfiguration( - // vcProvider - // ); + if (typeof env.vcProvider !== 'string') { + throw new Error("vcProvider not available in context"); + } + + const vcConfiguration = await getVerifiableCredentialApiConfiguration( + env.vcProvider.toString() + ); + + if (typeof vcConfiguration.issuerService !== "string" || typeof vcConfiguration.derivationService !== "string" || typeof vcConfiguration.statusService !== "string" || typeof vcConfiguration.verifierService !== "string") { + throw new Error("A service endpoint is undefined"); + } + + issuerService = vcConfiguration.issuerService; + derivationService = vcConfiguration.derivationService; + statusService = vcConfiguration.statusService; + verifierService = vcConfiguration.verifierService; }); afterEach(async () => { @@ -112,7 +126,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { describe("issue a VC", () => { it("Successfully gets a VC from a valid issuer", async () => { const credential = await issueVerifiableCredential( - new URL("issue", env.vcProvider).href, + issuerService, validSubjectClaims(), validCredentialClaims, { @@ -121,7 +135,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { ); expect(credential.credentialSubject.id).toBe(vcSubject); await revokeVerifiableCredential( - new URL("status", env.vcProvider).href, + statusService, credential.id, { fetch: session.fetch, @@ -135,7 +149,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { // associated to the preconfigured shape. it("throws if the issuer returns an error", async () => { const vcPromise = issueVerifiableCredential( - new URL("issue", env.vcProvider).href, + issuerService, invalidCredentialClaims, undefined, { @@ -150,7 +164,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { it("returns all VC issued matching a given shape", async () => { const [credential1, credential2] = await Promise.all([ issueVerifiableCredential( - new URL("issue", env.vcProvider).href, + issuerService, validSubjectClaims({ resource: "https://example.org/some-resource" }), validCredentialClaims, { @@ -158,7 +172,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { }, ), issueVerifiableCredential( - new URL("issue", env.vcProvider).href, + issuerService, validSubjectClaims({ resource: "https://example.org/another-resource", }), @@ -171,7 +185,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { await expect( getVerifiableCredentialAllFromShape( - new URL("derive", env.vcProvider).href, + derivationService, { "@context": [ "https://www.w3.org/2018/credentials/v1", @@ -187,7 +201,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { await expect( getVerifiableCredentialAllFromShape( - new URL("derive", env.vcProvider).href, + derivationService, credential1, { fetch: session.fetch, @@ -197,7 +211,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { await expect( getVerifiableCredentialAllFromShape( - new URL("derive", env.vcProvider).href, + derivationService, credential2, { fetch: session.fetch, @@ -207,14 +221,14 @@ describe("End-to-end verifiable credentials tests for environment", () => { await Promise.all([ revokeVerifiableCredential( - new URL("status", env.vcProvider).href, + statusService, credential1.id, { fetch: session.fetch, }, ), revokeVerifiableCredential( - new URL("status", env.vcProvider).href, + statusService, credential2.id, { fetch: session.fetch, @@ -227,7 +241,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { describe("revoke VCs", () => { it("can revoke a VC", async () => { const credential = await issueVerifiableCredential( - new URL("issue", env.vcProvider).href, + issuerService, validSubjectClaims(), validCredentialClaims, { @@ -236,7 +250,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { ); await expect( revokeVerifiableCredential( - new URL("status", env.vcProvider).href, + statusService, credential.id, { fetch: session.fetch, @@ -244,7 +258,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { ), ).resolves.not.toThrow(); const verificationResponse = await session.fetch( - new URL("verify", env.vcProvider).href, + verifierService, { method: "POST", body: JSON.stringify({ verifiableCredential: credential }), From 57b8b001bc9f50b8b9782bfbf55b18ab341d1f6a Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 25 Nov 2023 11:07:40 +0000 Subject: [PATCH 41/79] chore: run lint:fix --- e2e/node/e2e.test.ts | 93 +++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 58 deletions(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index 598f9c44..4739893e 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -22,19 +22,18 @@ // FIXME: Remove when refactoring to test matrix /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { describe, it, expect, beforeEach, afterEach } from "@jest/globals"; -import type { Session } from "@inrupt/solid-client-authn-node"; import { - getNodeTestingEnvironment, getAuthenticatedSession, + getNodeTestingEnvironment, } from "@inrupt/internal-test-env"; +import type { Session } from "@inrupt/solid-client-authn-node"; +import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; import { getVerifiableCredentialAllFromShape, getVerifiableCredentialApiConfiguration, issueVerifiableCredential, revokeVerifiableCredential, } from "../../src/index"; -import { VerifiableCredentialApiConfiguration } from "../../src/common/common"; const validCredentialClaims = { "@context": [ @@ -99,15 +98,20 @@ describe("End-to-end verifiable credentials tests for environment", () => { vcSubject = session.info.webId; } - if (typeof env.vcProvider !== 'string') { + if (typeof env.vcProvider !== "string") { throw new Error("vcProvider not available in context"); } const vcConfiguration = await getVerifiableCredentialApiConfiguration( - env.vcProvider.toString() + env.vcProvider.toString(), ); - if (typeof vcConfiguration.issuerService !== "string" || typeof vcConfiguration.derivationService !== "string" || typeof vcConfiguration.statusService !== "string" || typeof vcConfiguration.verifierService !== "string") { + if ( + typeof vcConfiguration.issuerService !== "string" || + typeof vcConfiguration.derivationService !== "string" || + typeof vcConfiguration.statusService !== "string" || + typeof vcConfiguration.verifierService !== "string" + ) { throw new Error("A service endpoint is undefined"); } @@ -134,13 +138,9 @@ describe("End-to-end verifiable credentials tests for environment", () => { }, ); expect(credential.credentialSubject.id).toBe(vcSubject); - await revokeVerifiableCredential( - statusService, - credential.id, - { - fetch: session.fetch, - }, - ); + await revokeVerifiableCredential(statusService, credential.id, { + fetch: session.fetch, + }); }); // FIXME: based on configuration, the server may have one of two behaviors @@ -200,40 +200,24 @@ describe("End-to-end verifiable credentials tests for environment", () => { ).resolves.not.toHaveLength(0); await expect( - getVerifiableCredentialAllFromShape( - derivationService, - credential1, - { - fetch: session.fetch, - }, - ), + getVerifiableCredentialAllFromShape(derivationService, credential1, { + fetch: session.fetch, + }), ).resolves.toHaveLength(1); await expect( - getVerifiableCredentialAllFromShape( - derivationService, - credential2, - { - fetch: session.fetch, - }, - ), + getVerifiableCredentialAllFromShape(derivationService, credential2, { + fetch: session.fetch, + }), ).resolves.toHaveLength(1); await Promise.all([ - revokeVerifiableCredential( - statusService, - credential1.id, - { - fetch: session.fetch, - }, - ), - revokeVerifiableCredential( - statusService, - credential2.id, - { - fetch: session.fetch, - }, - ), + revokeVerifiableCredential(statusService, credential1.id, { + fetch: session.fetch, + }), + revokeVerifiableCredential(statusService, credential2.id, { + fetch: session.fetch, + }), ]); }, 60_000); }); @@ -249,24 +233,17 @@ describe("End-to-end verifiable credentials tests for environment", () => { }, ); await expect( - revokeVerifiableCredential( - statusService, - credential.id, - { - fetch: session.fetch, - }, - ), + revokeVerifiableCredential(statusService, credential.id, { + fetch: session.fetch, + }), ).resolves.not.toThrow(); - const verificationResponse = await session.fetch( - verifierService, - { - method: "POST", - body: JSON.stringify({ verifiableCredential: credential }), - headers: { - "Content-Type": "application/json", - }, + const verificationResponse = await session.fetch(verifierService, { + method: "POST", + body: JSON.stringify({ verifiableCredential: credential }), + headers: { + "Content-Type": "application/json", }, - ); + }); const verification = await verificationResponse.json(); expect(verification.errors).toEqual([ "credentialStatus validation has failed: credential has been revoked", From 340b51b4a5cba11a35812c9bb53fff8e66534586 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 25 Nov 2023 12:55:13 +0000 Subject: [PATCH 42/79] chore: fix timeouts in e2e tests and use automatically fetched config --- e2e/node/e2e.test.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index 4739893e..91538590 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -27,7 +27,7 @@ import { getNodeTestingEnvironment, } from "@inrupt/internal-test-env"; import type { Session } from "@inrupt/solid-client-authn-node"; -import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; +import { describe, it, expect, beforeAll, afterAll } from "@jest/globals"; import { getVerifiableCredentialAllFromShape, getVerifiableCredentialApiConfiguration, @@ -42,7 +42,10 @@ const validCredentialClaims = { ], type: ["SolidAccessRequest"], }; -const validSubjectClaims = (options?: { resource?: string }) => ({ +const validSubjectClaims = (options?: { + resource?: string; + purpose?: string; +}) => ({ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://schema.inrupt.com/credentials/v1.jsonld", @@ -50,7 +53,7 @@ const validSubjectClaims = (options?: { resource?: string }) => ({ hasConsent: { mode: "Read", forPersonalData: options?.resource ?? "https://example.org/ns/someData", - forPurpose: "https://example.org/ns/somePurpose", + forPurpose: options?.purpose ?? "https://example.org/ns/somePurpose", hasStatus: "ConsentStatusRequested", isConsentForDataSubject: "https://some.webid/resource-owner", }, @@ -89,7 +92,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { let statusService: string; let verifierService: string; - beforeEach(async () => { + beforeAll(async () => { session = await getAuthenticatedSession(env); if (!session.info.webId) { @@ -121,7 +124,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { verifierService = vcConfiguration.verifierService; }); - afterEach(async () => { + afterAll(async () => { // Making sure the session is logged out prevents tests from hanging due // to the callback refreshing the access token. await session.logout(); @@ -162,10 +165,14 @@ describe("End-to-end verifiable credentials tests for environment", () => { describe("lookup VCs", () => { it("returns all VC issued matching a given shape", async () => { + const purpose = `http://example.org/some/purpose/${Date.now()}`; const [credential1, credential2] = await Promise.all([ issueVerifiableCredential( issuerService, - validSubjectClaims({ resource: "https://example.org/some-resource" }), + validSubjectClaims({ + resource: "https://example.org/some-resource", + purpose, + }), validCredentialClaims, { fetch: session.fetch, @@ -175,6 +182,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { issuerService, validSubjectClaims({ resource: "https://example.org/another-resource", + purpose, }), validCredentialClaims, { @@ -192,12 +200,19 @@ describe("End-to-end verifiable credentials tests for environment", () => { "https://schema.inrupt.com/credentials/v1.jsonld", ], type: ["VerifiableCredential"], + credentialSubject: { + id: vcSubject, + hasConsent: { + forPurpose: purpose, + }, + }, }, { fetch: session.fetch, + includeExpiredVc: false, }, ), - ).resolves.not.toHaveLength(0); + ).resolves.toHaveLength(2); await expect( getVerifiableCredentialAllFromShape(derivationService, credential1, { From c049814f9d2a80d11bc902e6c85f0dec1f868924 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 25 Nov 2023 16:29:19 +0000 Subject: [PATCH 43/79] WIP: implement normalization opt-out --- src/common/common.test.ts | 42 +++++---- src/common/common.ts | 171 ++++++++++++++++++++++++++++++++++--- src/common/getters.test.ts | 21 +++++ src/common/getters.ts | 2 +- src/issue/issue.test.ts | 4 +- src/issue/issue.ts | 97 ++++++++++++++++----- src/lookup/derive.ts | 43 +++++++++- src/lookup/query.ts | 41 ++++++++- src/parser/jsonld.ts | 2 +- 9 files changed, 366 insertions(+), 57 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 874abb73..b32e8c56 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -91,18 +91,28 @@ describe("isVerifiableCredential", () => { ["proofPurpose"], ["proofValue"], ])("is missing field %s", async (entry) => { - expect( - getters.isVerifiableCredential( - await verifiableCredentialToDataset( - mockPartialCredential({ - ...defaultCredentialClaims, - [`${entry}`]: undefined, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }) as any, + if (entry !== 'id') { + expect( + getters.isVerifiableCredential( + await verifiableCredentialToDataset( + mockPartialCredential({ + ...defaultCredentialClaims, + [`${entry}`]: undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + ), + namedNode(mockDefaultCredential().id), ), - namedNode(mockDefaultCredential().id), - ), - ).toBe(false); + ).toBe(false); + } else { + expect(verifiableCredentialToDataset( + mockPartialCredential({ + ...defaultCredentialClaims, + [`${entry}`]: undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + )).rejects.toThrow('Expected vc.id to be a string, found [undefined] of type [undefined]'); + } expect( isVerifiableCredential( @@ -194,7 +204,7 @@ describe("isVerifiablePresentation", () => { expect(isVerifiablePresentation(mockDefaultPresentation())).toBe(true); expect( getters.isVerifiablePresentation( - await verifiableCredentialToDataset(mockDefaultPresentation()), + await verifiableCredentialToDataset(mockDefaultPresentation() as { id: string }), namedNode(mockDefaultPresentation().id!), ), ).toBe(true); @@ -204,7 +214,7 @@ describe("isVerifiablePresentation", () => { expect(isVerifiablePresentation(mockDefaultPresentation([]))).toBe(true); expect( getters.isVerifiablePresentation( - await verifiableCredentialToDataset(mockDefaultPresentation([])), + await verifiableCredentialToDataset(mockDefaultPresentation([]) as { id: string }), namedNode(mockDefaultPresentation([]).id!), ), ).toBe(true); @@ -216,7 +226,7 @@ describe("isVerifiablePresentation", () => { expect(isVerifiablePresentation(mockedPresentation)).toBe(true); expect( getters.isVerifiablePresentation( - await verifiableCredentialToDataset(mockedPresentation), + await verifiableCredentialToDataset(mockedPresentation as { id: string }), namedNode(mockedPresentation.id!), ), ).toBe(true); @@ -316,7 +326,7 @@ describe("isVerifiablePresentation", () => { ]); const mockedPresentationAsDataset = - await verifiableCredentialToDataset(mockedPresentation); + await verifiableCredentialToDataset(mockedPresentation as { id: string }); expect( mockedPresentationAsDataset.match(null, cred.verifiableCredential, null) .size, @@ -365,7 +375,7 @@ describe("isVerifiablePresentation", () => { expect(isVerifiablePresentation(mockedPresentation)).toBe(false); const presentationAsDataset = - await verifiableCredentialToDataset(mockedPresentation); + await verifiableCredentialToDataset(mockedPresentation as { id: string }); expect(presentationAsDataset.match(null, cred.holder, null).size).toBe(0); expect( getters.isVerifiablePresentation( diff --git a/src/common/common.ts b/src/common/common.ts index cccea8d7..38f82013 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -31,52 +31,88 @@ import { getThingAll, } from "@inrupt/solid-client"; import { fetch as uniFetch } from "@inrupt/universal-fetch"; -import type { DatasetCore } from "@rdfjs/types"; +import type { DatasetCore, Quad } from "@rdfjs/types"; +import { DataFactory } from "n3"; import type { ParseOptions } from "../parser/jsonld"; import { jsonLdToStore } from "../parser/jsonld"; +import { DatasetWithId } from "./getters"; +import { isVerifiableCredential as isRdfjsVerifiableCredential } from "../common/getters"; +const { namedNode } = DataFactory; export type Iri = string; /** * A JSON-LD document is a JSON document including an @context entry. The other * fields may contain any value. + * @deprecated Use RDFJS API instead */ export type JsonLd = { "@context": unknown; [property: string]: unknown; }; +/** + * @deprecated Use RDFJS API instead + */ type Proof = { + /** + * @deprecated Use RDFJS API instead + */ type: string; /** * ISO-8601 formatted date */ created: string; + /** + * @deprecated Use RDFJS API instead + */ verificationMethod: string; + /** + * @deprecated Use RDFJS API instead + */ proofPurpose: string; + /** + * @deprecated Use RDFJS API instead + */ proofValue: string; }; /** * A Verifiable Credential JSON-LD document, as specified by the W3C VC HTTP API. + * @deprecated Use RDFJS API instead */ export type VerifiableCredentialBase = JsonLd & { id: Iri; + /** + * @deprecated Use RDFJS API instead + */ type: Iri[]; + /** + * @deprecated Use RDFJS API instead + */ issuer: Iri; /** * ISO-8601 formatted date + * @deprecated Use RDFJS API instead */ issuanceDate: string; /** * Entity the credential makes claim about. + * @deprecated Use RDFJS API instead */ credentialSubject: { + /** + * @deprecated Use RDFJS API instead + */ id: Iri; /** * The claim set is open, as any RDF graph is suitable for a set of claims. + * @deprecated Use RDFJS API instead */ [property: string]: unknown; }; + /** + * @deprecated Use RDFJS API instead + */ proof: Proof; }; @@ -411,10 +447,24 @@ export async function getVerifiableCredentialApiConfiguration( /** * @hidden */ -export async function verifiableCredentialToDataset( +export async function verifiableCredentialToDataset( vc: T, - options?: ParseOptions, -): Promise { + options?: ParseOptions & { + includeVcProperties: true + }, +): Promise +export async function verifiableCredentialToDataset( + vc: T, + options?: ParseOptions & { + includeVcProperties?: boolean + }, +): Promise +export async function verifiableCredentialToDataset( + vc: T, + options?: ParseOptions & { + includeVcProperties?: boolean + }, +): Promise { let store: DatasetCore; try { store = await jsonLdToStore(vc, options); @@ -424,20 +474,23 @@ export async function verifiableCredentialToDataset( ); } + if (typeof vc.id !== 'string') { + throw new Error(`Expected vc.id to be a string, found [${vc.id}] of type [${typeof vc.id}]`) + } + return Object.freeze({ - ...vc, + id: vc.id, + ...(options?.includeVcProperties && vc), // Make this a DatasetCore without polluting the object with // all of the properties present in the N3.Store [Symbol.iterator]() { return store[Symbol.iterator](); }, - has(quad) { + has(quad: Quad) { return store.has(quad); }, - match(subject, predicate, object, graph) { - // We need to cast to DatasetCore because the N3.Store - // type uses an internal type for Term rather than the @rdfjs/types Term - return store.match(subject, predicate, object, graph); + match(...args: Parameters) { + return store.match(...args); }, add() { throw new Error("Cannot mutate this dataset"); @@ -462,15 +515,43 @@ export async function verifiableCredentialToDataset( * @param vcUrl The URL of the VC. * @param options Options to customize the function behavior. * - options.fetch: Specify a WHATWG-compatible authenticated fetch. + * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response * @returns The dereferenced VC if valid. Throws otherwise. * @since 0.4.0 + * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future + * versions of this library. */ export async function getVerifiableCredential( vcUrl: UrlString, options?: ParseOptions & { fetch?: typeof fetch; + returnLegacyJsonld?: true; }, -): Promise { +): Promise +/** + * Dereference a VC URL, and verify that the resulting content is valid. + * + * @param vcUrl The URL of the VC. + * @param options Options to customize the function behavior. + * - options.fetch: Specify a WHATWG-compatible authenticated fetch. + * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response + * @returns The dereferenced VC if valid. Throws otherwise. + * @since 0.4.0 + */ +export async function getVerifiableCredential( + vcUrl: UrlString, + options?: ParseOptions & { + fetch?: typeof fetch; + returnLegacyJsonld?: boolean + }, +): Promise +export async function getVerifiableCredential( + vcUrl: UrlString, + options?: ParseOptions & { + fetch?: typeof fetch; + returnLegacyJsonld?: boolean + }, +): Promise { const authFetch = options?.fetch ?? uniFetch; const response = await authFetch(vcUrl); @@ -480,18 +561,80 @@ export async function getVerifiableCredential( ); } + return internal_getVerifiableCredentialFromResponse(vcUrl, response, options) +} + +export async function internal_getVerifiableCredentialFromResponse( + vcUrl: UrlString | undefined, + response: Response, + options?: ParseOptions & { + returnLegacyJsonld?: true + }, +): Promise +export async function internal_getVerifiableCredentialFromResponse( + vcUrl: UrlString | undefined, + response: Response, + options?: ParseOptions & { + returnLegacyJsonld?: boolean + }, +): Promise +export async function internal_getVerifiableCredentialFromResponse( + vcUrl: UrlString | undefined, + response: Response, + options?: ParseOptions & { + returnLegacyJsonld?: boolean + }, +): Promise { + const returnLegacy = options?.returnLegacyJsonld !== false; let vc: unknown | VerifiableCredentialBase; try { - vc = normalizeVc(await response.json()); + vc = await response.json(); + + if (typeof vcUrl !== 'string') { + if (!isUnknownObject(vc) || !('id' in vc) || typeof vc.id !== 'string') { + throw new Error("Cannot establish id of verifiable credential"); + } + vcUrl = vc.id; + } + + if (returnLegacy) { + vc = normalizeVc(vc); + } } catch (e) { throw new Error( `Parsing the Verifiable Credential [${vcUrl}] as JSON failed: ${e}`, ); } - if (!isVerifiableCredential(vc)) { + + if (returnLegacy) { + if (!isVerifiableCredential(vc)) { + throw new Error( + `The value received from [${vcUrl}] is not a Verifiable Credential`, + ); + } + return verifiableCredentialToDataset(vc, { + allowContextFetching: options?.allowContextFetching, + baseIRI: options?.baseIRI, + contexts: options?.contexts, + includeVcProperties: true, + }); + } + + if (typeof vc !== 'object' || vc === null || !('id' in vc) || typeof vc.id !== 'string') { + throw new Error("Verifiable credential is not an object, or does not have an id"); + } + + const parsedVc = await verifiableCredentialToDataset(vc as { id: string }, { + allowContextFetching: options.allowContextFetching, + baseIRI: options.baseIRI, + contexts: options.contexts, + includeVcProperties: false, + }); + + if (!isRdfjsVerifiableCredential(parsedVc, namedNode(vcUrl))) { throw new Error( `The value received from [${vcUrl}] is not a Verifiable Credential`, ); } - return verifiableCredentialToDataset(vc, options); + return parsedVc; } diff --git a/src/common/getters.test.ts b/src/common/getters.test.ts index 7293a13a..6f60fb07 100644 --- a/src/common/getters.test.ts +++ b/src/common/getters.test.ts @@ -26,6 +26,7 @@ import { verifiableCredentialToDataset } from "./common"; import { cred, xsd } from "./constants"; import { mockDefaultCredential } from "./common.mock"; import { + DatasetWithId, getCredentialSubject, getExpirationDate, getId, @@ -37,21 +38,32 @@ const { quad, namedNode, blankNode, literal } = DataFactory; describe("getters", () => { let defaultCredential: VerifiableCredential; + let defaultCredentialNoProperties: DatasetWithId; beforeAll(async () => { defaultCredential = await verifiableCredentialToDataset( mockDefaultCredential(), + { + includeVcProperties: true + } + ); + defaultCredentialNoProperties = await verifiableCredentialToDataset( + mockDefaultCredential() ); }); it("getId", () => { expect(getId(defaultCredential)).toBe(defaultCredential.id); + expect(getId(defaultCredentialNoProperties)).toBe(defaultCredential.id); }); it("getIssuanceDate", () => { expect(getIssuanceDate(defaultCredential)).toStrictEqual( new Date(defaultCredential.issuanceDate), ); + expect(getIssuanceDate(defaultCredentialNoProperties)).toStrictEqual( + new Date(defaultCredential.issuanceDate), + ); }); it("getIssuanceDate errors if issuance date is not a literal", () => { @@ -117,6 +129,7 @@ describe("getters", () => { describe("getExpirationDate", () => { it("returns undefined if there is no expiration date", () => { expect(getExpirationDate(defaultCredential)).toBeUndefined(); + expect(getExpirationDate(defaultCredentialNoProperties)).toBeUndefined(); }); it("gets the access expiration date", async () => { @@ -189,6 +202,9 @@ describe("getters", () => { expect(getIssuer(defaultCredential)).toStrictEqual( defaultCredential.issuer, ); + expect(getIssuer(defaultCredentialNoProperties)).toStrictEqual( + defaultCredential.issuer, + ); }); it("getCredentialSubject", () => { @@ -196,6 +212,11 @@ describe("getters", () => { defaultCredential.credentialSubject.id, ); expect(getCredentialSubject(defaultCredential).termType).toBe("NamedNode"); + + expect(getCredentialSubject(defaultCredentialNoProperties).value).toStrictEqual( + defaultCredential.credentialSubject.id, + ); + expect(getCredentialSubject(defaultCredentialNoProperties).termType).toBe("NamedNode"); }); it("getCredentialSubject errors if there are multiple credential subjects", () => { diff --git a/src/common/getters.ts b/src/common/getters.ts index 50285df4..ef4f58a7 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -18,7 +18,7 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type { Literal, DatasetCore, NamedNode, BlankNode } from "@rdfjs/types"; +import type { Literal, DatasetCore, NamedNode, BlankNode, Quad } from "@rdfjs/types"; import { DataFactory } from "n3"; import { getSingleObject, lenientSingle } from "./rdfjs"; import { cred, xsd, dc, sec, rdf } from "./constants"; diff --git a/src/issue/issue.test.ts b/src/issue/issue.test.ts index 5414d6f9..cf49ce11 100644 --- a/src/issue/issue.test.ts +++ b/src/issue/issue.test.ts @@ -106,7 +106,7 @@ describe("issueVerifiableCredential", () => { { "@context": ["https://some.context"] }, { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, ), - ).rejects.toThrow("unexpected object: "); + ).rejects.toThrow("Parsing the Verifiable Credential [undefined] as JSON failed: Error: Cannot establish id of verifiable credential"); }); it("returns the VC issued by the target issuer", async () => { @@ -390,7 +390,7 @@ describe("issueVerifiableCredential", () => { }); it("normalizes the issued VC", async () => { - const mockedVc = mockDefaultCredential(); + const mockedVc = mockDefaultCredential('http://example.org/my/sample/id'); // Force unexpected VC shapes to check normalization. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/src/issue/issue.ts b/src/issue/issue.ts index 4f89b51f..eb3436d8 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -30,14 +30,14 @@ import { concatenateContexts, defaultContext, defaultCredentialTypes, - isVerifiableCredential, - normalizeVc, - verifiableCredentialToDataset, + internal_getVerifiableCredentialFromResponse, } from "../common/common"; import type { ParseOptions } from "../parser/jsonld"; +import { DatasetWithId } from "../common/getters"; type OptionsType = { fetch?: typeof fallbackFetch; + returnLegacyJsonld?: boolean; }; // The following extracts the logic of issueVerifiableCredential to separate it @@ -48,8 +48,29 @@ async function internal_issueVerifiableCredential( issuerEndpoint: Iri, subjectClaims: JsonLd, credentialClaims?: JsonLd, - options?: OptionsType & ParseOptions, -): Promise { + options?: { + fetch?: typeof fallbackFetch; + returnLegacyJsonld?: true; + } & ParseOptions, +): Promise +async function internal_issueVerifiableCredential( + issuerEndpoint: Iri, + subjectClaims: JsonLd, + credentialClaims?: JsonLd, + options?: { + fetch?: typeof fallbackFetch; + returnLegacyJsonld?: boolean; + } & ParseOptions, +): Promise +async function internal_issueVerifiableCredential( + issuerEndpoint: Iri, + subjectClaims: JsonLd, + credentialClaims?: JsonLd, + options?: { + fetch?: typeof fallbackFetch; + returnLegacyJsonld?: boolean; + } & ParseOptions, +): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { internalOptions.fetch = fallbackFetch; @@ -104,17 +125,7 @@ async function internal_issueVerifiableCredential( ); } - const jsonData = normalizeVc(await response.json()); - if (isVerifiableCredential(jsonData)) { - return verifiableCredentialToDataset(jsonData); - } - throw new Error( - `The VC issuing endpoint [${issuerEndpoint}] returned an unexpected object: ${JSON.stringify( - jsonData, - null, - " ", - )}`, - ); + return internal_getVerifiableCredentialFromResponse(undefined, response, options); } /** @@ -125,19 +136,52 @@ async function internal_issueVerifiableCredential( * @param subjectId The identifier of the VC claims' subject. * @param subjectClaims Claims about the subject that will be attested by the VC. * @param credentialClaims Claims about the credential itself, rather than its subject, e.g. credential type or expiration. - * @param options Optional parameter `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). + * @param options + * - options.fetch: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). * This can be typically used for authentication. Note that if it is omitted, and * `@inrupt/solid-client-authn-browser` is in your dependencies, the default session * is picked up. + * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response * @returns the VC returned by the Issuer if the request is successful. Otherwise, an error is thrown. * @since 0.1.0 + * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future + * versions of this library. */ export async function issueVerifiableCredential( issuerEndpoint: Iri, subjectClaims: JsonLd, credentialClaims?: JsonLd, - options?: OptionsType, + options?: { + fetch?: typeof fallbackFetch; + returnLegacyJsonld?: true; + }, ): Promise; +/** + * Request that a given Verifiable Credential (VC) Issuer issues a VC containing + * the provided claims. The VC Issuer is expected to implement the [W3C VC Issuer HTTP API](https://w3c-ccg.github.io/vc-api/issuer.html). + * + * @param issuerEndpoint The `/issue` endpoint of the VC Issuer. + * @param subjectId The identifier of the VC claims' subject. + * @param subjectClaims Claims about the subject that will be attested by the VC. + * @param credentialClaims Claims about the credential itself, rather than its subject, e.g. credential type or expiration. + * @param options + * - options.fetch: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). + * This can be typically used for authentication. Note that if it is omitted, and + * `@inrupt/solid-client-authn-browser` is in your dependencies, the default session + * is picked up. + * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response + * @returns the VC returned by the Issuer if the request is successful. Otherwise, an error is thrown. + * @since 0.1.0 + */ +export async function issueVerifiableCredential( + issuerEndpoint: Iri, + subjectClaims: JsonLd, + credentialClaims?: JsonLd, + options?: { + fetch?: typeof fallbackFetch; + returnLegacyJsonld?: boolean; + }, +): Promise; /** * @deprecated Please remove the `subjectId` parameter */ @@ -146,8 +190,21 @@ export async function issueVerifiableCredential( subjectId: Iri, subjectClaims: JsonLd, credentialClaims?: JsonLd, - options?: OptionsType, + options?: { + fetch?: typeof fallbackFetch; + returnLegacyJsonld?: true; + }, ): Promise; +/** + * @deprecated Please remove the `subjectId` parameter + */ +export async function issueVerifiableCredential( + issuerEndpoint: Iri, + subjectId: Iri, + subjectClaims: JsonLd, + credentialClaims?: JsonLd, + options?: OptionsType, +): Promise; // The signature of the implementation here is a bit confusing, but it avoid // breaking changes until we remove the `subjectId` completely from the API export async function issueVerifiableCredential( @@ -156,7 +213,7 @@ export async function issueVerifiableCredential( subjectOrCredentialClaims: JsonLd | undefined, credentialClaimsOrOptions?: JsonLd | OptionsType, options?: OptionsType, -): Promise { +): Promise { if (typeof subjectIdOrClaims === "string") { // The function has been called with the deprecated signature, and the // subjectOrCredentialClaims parameter should be ignored. diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index 300fa581..5aa29747 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -28,6 +28,7 @@ import type { import { concatenateContexts, defaultContext } from "../common/common"; import type { VerifiablePresentationRequest } from "./query"; import { query } from "./query"; +import { DatasetWithId } from "../common/getters"; const INCLUDE_EXPIRED_VC_OPTION = "ExpiredVerifiableCredential" as const; @@ -93,9 +94,11 @@ function buildQueryByExample( * is picked up. * - `options.includeExpiredVc`: include expired VC matching the shape in the * result set. + * - `options.returnLegacyJsonld`: : Include the normalized JSON-LD in the response * @returns A list of VCs matching the given VC shape. The list may be empty if * the holder does not hold any matching VC. * @since 0.1.0 + * @deprecated set options.includeExpiredVc to false and use RDFJS API instead */ export async function getVerifiableCredentialAllFromShape( holderEndpoint: Iri, @@ -103,8 +106,46 @@ export async function getVerifiableCredentialAllFromShape( options?: Partial<{ fetch: typeof fallbackFetch; includeExpiredVc: boolean; + returnLegacyJsonld?: true; }>, -): Promise { +): Promise +/** + * Look up VCs from a given holder according to a subset of their claims, such as + * the VC type, or any property associated to the subject in the VC. The holder + * is expected to implement the [W3C VC Holder HTTP API](https://w3c-ccg.github.io/vc-api/holder.html). + * + * @param holderEndpoint The `/derive` endpoint of the holder. + * @param vcShape The subset of claims you expect the matching VCs to contain. + * @param options Optional parameter: + * - `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). + * This can be typically used for authentication. Note that if it is omitted, and + * `@inrupt/solid-client-authn-browser` is in your dependencies, the default session + * is picked up. + * - `options.includeExpiredVc`: include expired VC matching the shape in the + * result set. + * - `options.returnLegacyJsonld`: : Include the normalized JSON-LD in the response + * @returns A list of VCs matching the given VC shape. The list may be empty if + * the holder does not hold any matching VC. + * @since 0.1.0 + */ +export async function getVerifiableCredentialAllFromShape( + holderEndpoint: Iri, + vcShape: Partial, + options?: Partial<{ + fetch: typeof fallbackFetch; + includeExpiredVc: boolean; + returnLegacyJsonld: false; + }>, +): Promise +export async function getVerifiableCredentialAllFromShape( + holderEndpoint: Iri, + vcShape: Partial, + options?: Partial<{ + fetch: typeof fallbackFetch; + includeExpiredVc: boolean; + returnLegacyJsonld?: boolean; + }>, +): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { internalOptions.fetch = fallbackFetch; diff --git a/src/lookup/query.ts b/src/lookup/query.ts index c33e9417..b1e3cd60 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -32,6 +32,7 @@ import { verifiableCredentialToDataset, } from "../common/common"; import type { ParseOptions } from "../parser/jsonld"; +import { DatasetWithId } from "../common/getters"; /** * Based on https://w3c-ccg.github.io/vp-request-spec/#query-by-example. @@ -71,6 +72,18 @@ interface ParsedVerifiablePresentation extends VerifiablePresentation { verifiableCredential?: VerifiableCredential[]; } +/** + * @deprecated Use RDFJS API + */ +export async function query( + queryEndpoint: Iri, + vpRequest: VerifiablePresentationRequest, + options?: ParseOptions & + Partial<{ + fetch: typeof fallbackFetch; + returnLegacyJsonld?: false; + }>, +): Promise /** * Send a Verifiable Presentation Request to a query endpoint in order to retrieve * all Verifiable Credentials matching the query, wrapped in a single Presentation. @@ -109,8 +122,18 @@ export async function query( options?: ParseOptions & Partial<{ fetch: typeof fallbackFetch; + returnLegacyJsonld: false; + }>, +): Promise +export async function query( + queryEndpoint: Iri, + vpRequest: VerifiablePresentationRequest, + options?: ParseOptions & + Partial<{ + fetch: typeof fallbackFetch; + returnLegacyJsonld?: boolean; }>, -): Promise { +): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { internalOptions.fetch = fallbackFetch; @@ -128,6 +151,17 @@ export async function query( ); } + if (options?.returnLegacyJsonld === false) { + try { + return verifiableCredentialToDataset(await response.json()); + } catch (e) { + throw new Error( + `The holder [${queryEndpoint}] did not return a valid JSON response: parsing failed with error ${e}`, + ); + } + } + + // All code below here should is deprecated let data; try { data = normalizeVp(await response.json()); @@ -156,7 +190,10 @@ export async function query( ...(await Promise.all( data.verifiableCredential .slice(i, i + 100) - .map((vc) => verifiableCredentialToDataset(vc, options)), + .map((vc) => verifiableCredentialToDataset(vc, { + ...options, + includeVcProperties: true + })), )), ); } diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index f10f47fe..ab428309 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -193,6 +193,6 @@ export async function jsonLdStringToStore( * @param options An optional fetch function for dereferencing remote contexts * @returns A store containing the Quads in the JSON-LD response */ -export function jsonLdToStore(data: JsonLd, options?: ParseOptions) { +export function jsonLdToStore(data: unknown, options?: ParseOptions) { return jsonLdStringToStore(JSON.stringify(data), options); } From 686fa571c5e9e5568961f6792725b189cf6bdaec Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 25 Nov 2023 19:18:22 +0000 Subject: [PATCH 44/79] WIP: enable normalization opt out --- e2e/node/e2e.test.ts | 181 ++++++++++++++++++++++++++++++++----- src/common/common.test.ts | 47 ++++++---- src/common/common.ts | 83 +++++++++++------ src/common/getters.test.ts | 16 ++-- src/common/getters.ts | 13 ++- src/issue/issue.test.ts | 6 +- src/issue/issue.ts | 16 ++-- src/lookup/derive.ts | 18 +++- src/lookup/query.ts | 124 +++++++++++++++++++------ 9 files changed, 388 insertions(+), 116 deletions(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index 91538590..3634d6d6 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -29,6 +29,7 @@ import { import type { Session } from "@inrupt/solid-client-authn-node"; import { describe, it, expect, beforeAll, afterAll } from "@jest/globals"; import { + getCredentialSubject, getVerifiableCredentialAllFromShape, getVerifiableCredentialApiConfiguration, issueVerifiableCredential, @@ -141,9 +142,75 @@ describe("End-to-end verifiable credentials tests for environment", () => { }, ); expect(credential.credentialSubject.id).toBe(vcSubject); - await revokeVerifiableCredential(statusService, credential.id, { - fetch: session.fetch, - }); + expect(getCredentialSubject(credential).value).toBe(vcSubject); + + const credentialWithSubject = await issueVerifiableCredential( + issuerService, + "http://example.org/my/subject/id", + validSubjectClaims(), + validCredentialClaims, + + { + fetch: session.fetch, + }, + ); + expect(credentialWithSubject.credentialSubject.id).toBe(vcSubject); + expect(getCredentialSubject(credentialWithSubject).value).toBe(vcSubject); + + const credentialWithSubjectNoLegacyJson = await issueVerifiableCredential( + issuerService, + "http://example.org/my/subject/id", + validSubjectClaims(), + validCredentialClaims, + + { + fetch: session.fetch, + returnLegacyJsonld: false, + }, + ); + + expect( + // @ts-expect-error the credentialSubject property should not exist if legacy json is disabled + credentialWithSubjectNoLegacyJson.credentialSubject, + ).toBeUndefined(); + expect( + getCredentialSubject(credentialWithSubjectNoLegacyJson).value, + ).toBe(vcSubject); + + const credentialNoProperties = await issueVerifiableCredential( + issuerService, + validSubjectClaims(), + validCredentialClaims, + { + fetch: session.fetch, + returnLegacyJsonld: false, + }, + ); + + // @ts-expect-error the credentialSubject property should not exist if legacy json is disabled + expect(credentialNoProperties.credentialSubject).toBeUndefined(); + expect(getCredentialSubject(credentialNoProperties).value).toBe( + vcSubject, + ); + + await Promise.all([ + revokeVerifiableCredential(statusService, credential.id, { + fetch: session.fetch, + }), + revokeVerifiableCredential(statusService, credentialWithSubject.id, { + fetch: session.fetch, + }), + revokeVerifiableCredential( + statusService, + credentialWithSubjectNoLegacyJson.id, + { + fetch: session.fetch, + }, + ), + revokeVerifiableCredential(statusService, credentialNoProperties.id, { + fetch: session.fetch, + }), + ]); }); // FIXME: based on configuration, the server may have one of two behaviors @@ -191,38 +258,108 @@ describe("End-to-end verifiable credentials tests for environment", () => { ), ]); - await expect( - getVerifiableCredentialAllFromShape( - derivationService, - { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.inrupt.com/credentials/v1.jsonld", - ], - type: ["VerifiableCredential"], - credentialSubject: { - id: vcSubject, - hasConsent: { - forPurpose: purpose, - }, + expect(credential1.credentialSubject.id).toBe(vcSubject); + expect(getCredentialSubject(credential1).value).toBe( + vcSubject, + ); + expect(credential2.credentialSubject.id).toBe(vcSubject); + expect(getCredentialSubject(credential2).value).toBe( + vcSubject, + ); + + const allDeprecated = await getVerifiableCredentialAllFromShape( + derivationService, + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.inrupt.com/credentials/v1.jsonld", + ], + type: ["VerifiableCredential"], + credentialSubject: { + id: vcSubject, + hasConsent: { + forPurpose: purpose, }, }, - { - fetch: session.fetch, - includeExpiredVc: false, + }, + { + fetch: session.fetch, + includeExpiredVc: false, + }, + ); + + expect(allDeprecated).toHaveLength(2); + + + expect(allDeprecated[0].credentialSubject.id).toBe(vcSubject); + expect(getCredentialSubject(allDeprecated[0]).value).toBe( + vcSubject, + ); + expect(allDeprecated[1].credentialSubject.id).toBe(vcSubject); + expect(getCredentialSubject(allDeprecated[1]).value).toBe( + vcSubject, + ); + + const allNew = await getVerifiableCredentialAllFromShape( + derivationService, + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.inrupt.com/credentials/v1.jsonld", + ], + type: ["VerifiableCredential"], + credentialSubject: { + id: vcSubject, + hasConsent: { + forPurpose: purpose, + }, }, - ), - ).resolves.toHaveLength(2); + }, + { + fetch: session.fetch, + includeExpiredVc: false, + returnLegacyJsonld: false, + }, + ) + + expect(allNew).toHaveLength(2); + + // @ts-expect-error the credentialSubject property should not exist if legacy json is disabled + expect(allNew[0].credentialSubject).toBe(undefined); + expect(getCredentialSubject(allNew[0]).value).toBe( + vcSubject, + ); + + // @ts-expect-error the credentialSubject property should not exist if legacy json is disabled + expect(allNew[1].credentialSubject).toBe(undefined); + expect(getCredentialSubject(allNew[1]).value).toBe( + vcSubject, + ); + + + await expect( + getVerifiableCredentialAllFromShape(derivationService, credential1, { + fetch: session.fetch, + }), + ).resolves.toHaveLength(1); + + await expect( + getVerifiableCredentialAllFromShape(derivationService, credential2, { + fetch: session.fetch, + }), + ).resolves.toHaveLength(1); await expect( getVerifiableCredentialAllFromShape(derivationService, credential1, { fetch: session.fetch, + returnLegacyJsonld: false, }), ).resolves.toHaveLength(1); await expect( getVerifiableCredentialAllFromShape(derivationService, credential2, { fetch: session.fetch, + returnLegacyJsonld: false, }), ).resolves.toHaveLength(1); diff --git a/src/common/common.test.ts b/src/common/common.test.ts index b32e8c56..13e8a9d3 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -91,7 +91,8 @@ describe("isVerifiableCredential", () => { ["proofPurpose"], ["proofValue"], ])("is missing field %s", async (entry) => { - if (entry !== 'id') { + if (entry !== "id") { + // eslint-disable-next-line jest/no-conditional-expect expect( getters.isVerifiableCredential( await verifiableCredentialToDataset( @@ -105,13 +106,18 @@ describe("isVerifiableCredential", () => { ), ).toBe(false); } else { - expect(verifiableCredentialToDataset( - mockPartialCredential({ - ...defaultCredentialClaims, - [`${entry}`]: undefined, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }) as any, - )).rejects.toThrow('Expected vc.id to be a string, found [undefined] of type [undefined]'); + // eslint-disable-next-line jest/no-conditional-expect + await expect( + verifiableCredentialToDataset( + mockPartialCredential({ + ...defaultCredentialClaims, + [`${entry}`]: undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + ), + ).rejects.toThrow( + "Expected vc.id to be a string, found [undefined] of type [undefined]", + ); } expect( @@ -204,7 +210,9 @@ describe("isVerifiablePresentation", () => { expect(isVerifiablePresentation(mockDefaultPresentation())).toBe(true); expect( getters.isVerifiablePresentation( - await verifiableCredentialToDataset(mockDefaultPresentation() as { id: string }), + await verifiableCredentialToDataset( + mockDefaultPresentation() as { id: string }, + ), namedNode(mockDefaultPresentation().id!), ), ).toBe(true); @@ -214,7 +222,9 @@ describe("isVerifiablePresentation", () => { expect(isVerifiablePresentation(mockDefaultPresentation([]))).toBe(true); expect( getters.isVerifiablePresentation( - await verifiableCredentialToDataset(mockDefaultPresentation([]) as { id: string }), + await verifiableCredentialToDataset( + mockDefaultPresentation([]) as { id: string }, + ), namedNode(mockDefaultPresentation([]).id!), ), ).toBe(true); @@ -226,7 +236,9 @@ describe("isVerifiablePresentation", () => { expect(isVerifiablePresentation(mockedPresentation)).toBe(true); expect( getters.isVerifiablePresentation( - await verifiableCredentialToDataset(mockedPresentation as { id: string }), + await verifiableCredentialToDataset( + mockedPresentation as { id: string }, + ), namedNode(mockedPresentation.id!), ), ).toBe(true); @@ -312,9 +324,8 @@ describe("isVerifiablePresentation", () => { ).toBe(false); expect( getters.isVerifiablePresentation( - // @ts-expect-error we expect that this is not a valid Verifiable Presentation await verifiableCredentialToDataset(vp), - // @ts-expect-error we expect that this is not a valid Verifiable Presentation + // @ts-expect-error id is of type unknown namedNode(vp.id), ), ).toBe(false); @@ -325,8 +336,9 @@ describe("isVerifiablePresentation", () => { {} as VerifiableCredential, ]); - const mockedPresentationAsDataset = - await verifiableCredentialToDataset(mockedPresentation as { id: string }); + const mockedPresentationAsDataset = await verifiableCredentialToDataset( + mockedPresentation as { id: string }, + ); expect( mockedPresentationAsDataset.match(null, cred.verifiableCredential, null) .size, @@ -374,8 +386,9 @@ describe("isVerifiablePresentation", () => { mockedPresentation.holder = "some non-URL holder"; expect(isVerifiablePresentation(mockedPresentation)).toBe(false); - const presentationAsDataset = - await verifiableCredentialToDataset(mockedPresentation as { id: string }); + const presentationAsDataset = await verifiableCredentialToDataset( + mockedPresentation as { id: string }, + ); expect(presentationAsDataset.match(null, cred.holder, null).size).toBe(0); expect( getters.isVerifiablePresentation( diff --git a/src/common/common.ts b/src/common/common.ts index 38f82013..94923b87 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -35,8 +35,9 @@ import type { DatasetCore, Quad } from "@rdfjs/types"; import { DataFactory } from "n3"; import type { ParseOptions } from "../parser/jsonld"; import { jsonLdToStore } from "../parser/jsonld"; -import { DatasetWithId } from "./getters"; -import { isVerifiableCredential as isRdfjsVerifiableCredential } from "../common/getters"; +import { isVerifiableCredential as isRdfjsVerifiableCredential } from "./getters"; +import type { DatasetWithId } from "./getters"; + const { namedNode } = DataFactory; export type Iri = string; @@ -447,22 +448,28 @@ export async function getVerifiableCredentialApiConfiguration( /** * @hidden */ -export async function verifiableCredentialToDataset( +export async function verifiableCredentialToDataset< + T extends Object & { id?: string }, +>( vc: T, options?: ParseOptions & { - includeVcProperties: true + includeVcProperties: true; }, -): Promise -export async function verifiableCredentialToDataset( +): Promise; +export async function verifiableCredentialToDataset< + T extends Object & { id?: string }, +>( vc: T, options?: ParseOptions & { - includeVcProperties?: boolean + includeVcProperties?: boolean; }, -): Promise -export async function verifiableCredentialToDataset( +): Promise; +export async function verifiableCredentialToDataset< + T extends Object & { id: string }, +>( vc: T, options?: ParseOptions & { - includeVcProperties?: boolean + includeVcProperties?: boolean; }, ): Promise { let store: DatasetCore; @@ -474,13 +481,28 @@ export async function verifiableCredentialToDataset( +vc: T, +store: DatasetCore, +options?: ParseOptions & { + includeVcProperties?: boolean; + additionalProperties?: Record; +}): DatasetWithId { return Object.freeze({ id: vc.id, ...(options?.includeVcProperties && vc), + ...options?.additionalProperties, // Make this a DatasetCore without polluting the object with // all of the properties present in the N3.Store [Symbol.iterator]() { @@ -489,7 +511,7 @@ export async function verifiableCredentialToDataset) { + match(...args: Parameters) { return store.match(...args); }, add() { @@ -527,7 +549,7 @@ export async function getVerifiableCredential( fetch?: typeof fetch; returnLegacyJsonld?: true; }, -): Promise +): Promise; /** * Dereference a VC URL, and verify that the resulting content is valid. * @@ -542,14 +564,14 @@ export async function getVerifiableCredential( vcUrl: UrlString, options?: ParseOptions & { fetch?: typeof fetch; - returnLegacyJsonld?: boolean + returnLegacyJsonld?: boolean; }, -): Promise +): Promise; export async function getVerifiableCredential( vcUrl: UrlString, options?: ParseOptions & { fetch?: typeof fetch; - returnLegacyJsonld?: boolean + returnLegacyJsonld?: boolean; }, ): Promise { const authFetch = options?.fetch ?? uniFetch; @@ -561,28 +583,28 @@ export async function getVerifiableCredential( ); } - return internal_getVerifiableCredentialFromResponse(vcUrl, response, options) + return internal_getVerifiableCredentialFromResponse(vcUrl, response, options); } export async function internal_getVerifiableCredentialFromResponse( vcUrl: UrlString | undefined, response: Response, options?: ParseOptions & { - returnLegacyJsonld?: true + returnLegacyJsonld?: true; }, -): Promise +): Promise; export async function internal_getVerifiableCredentialFromResponse( vcUrl: UrlString | undefined, response: Response, options?: ParseOptions & { - returnLegacyJsonld?: boolean + returnLegacyJsonld?: boolean; }, -): Promise +): Promise; export async function internal_getVerifiableCredentialFromResponse( vcUrl: UrlString | undefined, response: Response, options?: ParseOptions & { - returnLegacyJsonld?: boolean + returnLegacyJsonld?: boolean; }, ): Promise { const returnLegacy = options?.returnLegacyJsonld !== false; @@ -590,8 +612,8 @@ export async function internal_getVerifiableCredentialFromResponse( try { vc = await response.json(); - if (typeof vcUrl !== 'string') { - if (!isUnknownObject(vc) || !('id' in vc) || typeof vc.id !== 'string') { + if (typeof vcUrl !== "string") { + if (!isUnknownObject(vc) || !("id" in vc) || typeof vc.id !== "string") { throw new Error("Cannot establish id of verifiable credential"); } vcUrl = vc.id; @@ -620,8 +642,15 @@ export async function internal_getVerifiableCredentialFromResponse( }); } - if (typeof vc !== 'object' || vc === null || !('id' in vc) || typeof vc.id !== 'string') { - throw new Error("Verifiable credential is not an object, or does not have an id"); + if ( + typeof vc !== "object" || + vc === null || + !("id" in vc) || + typeof vc.id !== "string" + ) { + throw new Error( + "Verifiable credential is not an object, or does not have an id", + ); } const parsedVc = await verifiableCredentialToDataset(vc as { id: string }, { diff --git a/src/common/getters.test.ts b/src/common/getters.test.ts index 6f60fb07..26513195 100644 --- a/src/common/getters.test.ts +++ b/src/common/getters.test.ts @@ -25,8 +25,8 @@ import type { VerifiableCredential } from "./common"; import { verifiableCredentialToDataset } from "./common"; import { cred, xsd } from "./constants"; import { mockDefaultCredential } from "./common.mock"; +import type { DatasetWithId } from "./getters"; import { - DatasetWithId, getCredentialSubject, getExpirationDate, getId, @@ -44,11 +44,11 @@ describe("getters", () => { defaultCredential = await verifiableCredentialToDataset( mockDefaultCredential(), { - includeVcProperties: true - } + includeVcProperties: true, + }, ); defaultCredentialNoProperties = await verifiableCredentialToDataset( - mockDefaultCredential() + mockDefaultCredential(), ); }); @@ -213,10 +213,12 @@ describe("getters", () => { ); expect(getCredentialSubject(defaultCredential).termType).toBe("NamedNode"); - expect(getCredentialSubject(defaultCredentialNoProperties).value).toStrictEqual( - defaultCredential.credentialSubject.id, + expect( + getCredentialSubject(defaultCredentialNoProperties).value, + ).toStrictEqual(defaultCredential.credentialSubject.id); + expect(getCredentialSubject(defaultCredentialNoProperties).termType).toBe( + "NamedNode", ); - expect(getCredentialSubject(defaultCredentialNoProperties).termType).toBe("NamedNode"); }); it("getCredentialSubject errors if there are multiple credential subjects", () => { diff --git a/src/common/getters.ts b/src/common/getters.ts index ef4f58a7..6caa888a 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -18,11 +18,16 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type { Literal, DatasetCore, NamedNode, BlankNode, Quad } from "@rdfjs/types"; +import type { + BlankNode, + DatasetCore, + Literal, + NamedNode +} from "@rdfjs/types"; import { DataFactory } from "n3"; -import { getSingleObject, lenientSingle } from "./rdfjs"; -import { cred, xsd, dc, sec, rdf } from "./constants"; import { isUrl } from "./common"; +import { cred, dc, rdf, sec, xsd } from "./constants"; +import { getSingleObject, lenientSingle } from "./rdfjs"; const { namedNode, defaultGraph, quad } = DataFactory; @@ -222,7 +227,7 @@ export function isVerifiableCredential( export function isVerifiablePresentation( dataset: DatasetCore, - id: NamedNode, + id: NamedNode | BlankNode, ): boolean { for (const { object } of dataset.match( id, diff --git a/src/issue/issue.test.ts b/src/issue/issue.test.ts index cf49ce11..5a2f43a6 100644 --- a/src/issue/issue.test.ts +++ b/src/issue/issue.test.ts @@ -106,7 +106,9 @@ describe("issueVerifiableCredential", () => { { "@context": ["https://some.context"] }, { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, ), - ).rejects.toThrow("Parsing the Verifiable Credential [undefined] as JSON failed: Error: Cannot establish id of verifiable credential"); + ).rejects.toThrow( + "Parsing the Verifiable Credential [undefined] as JSON failed: Error: Cannot establish id of verifiable credential", + ); }); it("returns the VC issued by the target issuer", async () => { @@ -390,7 +392,7 @@ describe("issueVerifiableCredential", () => { }); it("normalizes the issued VC", async () => { - const mockedVc = mockDefaultCredential('http://example.org/my/sample/id'); + const mockedVc = mockDefaultCredential("http://example.org/my/sample/id"); // Force unexpected VC shapes to check normalization. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/src/issue/issue.ts b/src/issue/issue.ts index eb3436d8..75876f98 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -33,7 +33,7 @@ import { internal_getVerifiableCredentialFromResponse, } from "../common/common"; import type { ParseOptions } from "../parser/jsonld"; -import { DatasetWithId } from "../common/getters"; +import type { DatasetWithId } from "../common/getters"; type OptionsType = { fetch?: typeof fallbackFetch; @@ -52,7 +52,7 @@ async function internal_issueVerifiableCredential( fetch?: typeof fallbackFetch; returnLegacyJsonld?: true; } & ParseOptions, -): Promise +): Promise; async function internal_issueVerifiableCredential( issuerEndpoint: Iri, subjectClaims: JsonLd, @@ -61,7 +61,7 @@ async function internal_issueVerifiableCredential( fetch?: typeof fallbackFetch; returnLegacyJsonld?: boolean; } & ParseOptions, -): Promise +): Promise; async function internal_issueVerifiableCredential( issuerEndpoint: Iri, subjectClaims: JsonLd, @@ -125,7 +125,11 @@ async function internal_issueVerifiableCredential( ); } - return internal_getVerifiableCredentialFromResponse(undefined, response, options); + return internal_getVerifiableCredentialFromResponse( + undefined, + response, + options, + ); } /** @@ -136,7 +140,7 @@ async function internal_issueVerifiableCredential( * @param subjectId The identifier of the VC claims' subject. * @param subjectClaims Claims about the subject that will be attested by the VC. * @param credentialClaims Claims about the credential itself, rather than its subject, e.g. credential type or expiration. - * @param options + * @param options * - options.fetch: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). * This can be typically used for authentication. Note that if it is omitted, and * `@inrupt/solid-client-authn-browser` is in your dependencies, the default session @@ -164,7 +168,7 @@ export async function issueVerifiableCredential( * @param subjectId The identifier of the VC claims' subject. * @param subjectClaims Claims about the subject that will be attested by the VC. * @param credentialClaims Claims about the credential itself, rather than its subject, e.g. credential type or expiration. - * @param options + * @param options * - options.fetch: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). * This can be typically used for authentication. Note that if it is omitted, and * `@inrupt/solid-client-authn-browser` is in your dependencies, the default session diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index 5aa29747..bc902ef4 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -20,6 +20,7 @@ // import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; +import { DataFactory } from "n3"; import type { Iri, VerifiableCredential, @@ -28,7 +29,7 @@ import type { import { concatenateContexts, defaultContext } from "../common/common"; import type { VerifiablePresentationRequest } from "./query"; import { query } from "./query"; -import { DatasetWithId } from "../common/getters"; +import type { DatasetWithId } from "../common/getters"; const INCLUDE_EXPIRED_VC_OPTION = "ExpiredVerifiableCredential" as const; @@ -108,7 +109,7 @@ export async function getVerifiableCredentialAllFromShape( includeExpiredVc: boolean; returnLegacyJsonld?: true; }>, -): Promise +): Promise; /** * Look up VCs from a given holder according to a subset of their claims, such as * the VC type, or any property associated to the subject in the VC. The holder @@ -136,7 +137,7 @@ export async function getVerifiableCredentialAllFromShape( includeExpiredVc: boolean; returnLegacyJsonld: false; }>, -): Promise +): Promise; export async function getVerifiableCredentialAllFromShape( holderEndpoint: Iri, vcShape: Partial, @@ -162,10 +163,19 @@ export async function getVerifiableCredentialAllFromShape( options?.includeExpiredVc ?? false, // The legacy proprietary format is casted as a VP request to be passed to the `query` function. ) as unknown as VerifiablePresentationRequest); + + if (options?.returnLegacyJsonld === false) { + const vp = await query(holderEndpoint, vpRequest, { + fetch: options?.fetch ?? fallbackFetch, + returnLegacyJsonld: false + }); + return vp.verifiableCredential ?? []; + } + const vp = await query(holderEndpoint, vpRequest, { fetch: options?.fetch ?? fallbackFetch, }); - return vp.verifiableCredential ?? []; + return vp?.verifiableCredential ?? []; } export default getVerifiableCredentialAllFromShape; diff --git a/src/lookup/query.ts b/src/lookup/query.ts index b1e3cd60..51a1ab16 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -20,6 +20,7 @@ // import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; +import { DataFactory } from "n3"; import type { Iri, VerifiableCredential, @@ -27,12 +28,17 @@ import type { VerifiablePresentation, } from "../common/common"; import { + internal_applyDataset, isVerifiablePresentation, normalizeVp, verifiableCredentialToDataset, } from "../common/common"; -import type { ParseOptions } from "../parser/jsonld"; -import { DatasetWithId } from "../common/getters"; +import { jsonLdToStore, type ParseOptions } from "../parser/jsonld"; +import type { DatasetWithId } from "../common/getters"; +import { isRdfjsVerifiableCredential, isRdfjsVerifiablePresentation } from ".."; +import { cred, rdf } from "../common/constants"; + +const { namedNode, defaultGraph } = DataFactory; /** * Based on https://w3c-ccg.github.io/vp-request-spec/#query-by-example. @@ -81,9 +87,9 @@ export async function query( options?: ParseOptions & Partial<{ fetch: typeof fallbackFetch; - returnLegacyJsonld?: false; + returnLegacyJsonld?: true; }>, -): Promise +): Promise; /** * Send a Verifiable Presentation Request to a query endpoint in order to retrieve * all Verifiable Credentials matching the query, wrapped in a single Presentation. @@ -124,7 +130,7 @@ export async function query( fetch: typeof fallbackFetch; returnLegacyJsonld: false; }>, -): Promise +): Promise<{ verifiableCredential?: DatasetWithId[] }>; export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, @@ -133,7 +139,10 @@ export async function query( fetch: typeof fallbackFetch; returnLegacyJsonld?: boolean; }>, -): Promise { +): Promise< + | ParsedVerifiablePresentation + | { verifiableCredential?: DatasetWithId[] } +> { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { internalOptions.fetch = fallbackFetch; @@ -151,26 +160,77 @@ export async function query( ); } - if (options?.returnLegacyJsonld === false) { - try { - return verifiableCredentialToDataset(await response.json()); - } catch (e) { - throw new Error( - `The holder [${queryEndpoint}] did not return a valid JSON response: parsing failed with error ${e}`, - ); - } - } + // Return to this approach once https://github.com/rubensworks/jsonld-streaming-parser.js/issues/122 is resolved + // if (options?.returnLegacyJsonld === false) { + // try { + // const vpJson = await response.json(); + // console.log(JSON.stringify(vpJson, null, 2), null, 2) + // const store = await jsonLdToStore(vpJson); + // const vp = [...store.match(null, rdf.type, cred.VerifiablePresentation, defaultGraph())] + + // if (vp.length !== 1) { + // throw new Error(`Expected exactly 1 Verifiable Presentation. Found ${vp.length}.`) + // } + + // const [{ subject }] = vp; + + // if (subject.termType !== 'BlankNode' && subject.termType !== 'NamedNode') { + // throw new Error(`Expected VP to be a Blank Node or Named Node. Found [${subject.value}] of type [${subject.termType}].`) + // } - // All code below here should is deprecated + // if (!isRdfjsVerifiablePresentation(store, subject)) { + // throw new Error( + // `The holder [${queryEndpoint}] did not return a Verifiable Presentation: ${JSON.stringify( + // vpJson, null, 2 + // )}`, + // ); + // } + + // // In the future we want to get rid of this and get the verifiableCredential ids from the store + // // the reason we need this for now is because we need the verifiableCredential JSON object for + // // the toJSON method. + // const verifiableCredential: DatasetWithId[] = vpJson.verifiableCredential.map((vc: unknown) => { + // if (vc === null || typeof vc !== 'object') { + // throw new Error(`Verifiable Credential entry is not an object`); + // } + + // if (!('id' in vc) || typeof vc.id !== 'string') { + // throw new Error(`Verifiable credential is missing a string id`); + // } + + // const c = internal_applyDataset(vc as { id: string }, store, options) + + // if (!isRdfjsVerifiableCredential(store, namedNode(c.id))) { + // throw new Error(`[${c.id}] is not a valid Verifiable Credential`); + // } + // }); + // return internal_applyDataset(vpJson, store, { + // ...options, + // additionalProperties: { + // verifiableCredential + // } + // }); + // } catch (e) { + // throw new Error( + // `The holder [${queryEndpoint}] did not return a valid JSON response: parsing failed with error ${e}`, + // ); + // } + // } + + // All code below here should is deprecated let data; try { - data = normalizeVp(await response.json()); + data = await response.json() + + if (options?.returnLegacyJsonld !== false) { + data = normalizeVp(data); + } } catch (e) { throw new Error( `The holder [${queryEndpoint}] did not return a valid JSON response: parsing failed with error ${e}`, ); } - if (!isVerifiablePresentation(data)) { + if (options?.returnLegacyJsonld !== false && !isVerifiablePresentation(data)) { throw new Error( `The holder [${queryEndpoint}] did not return a Verifiable Presentation: ${JSON.stringify( data, @@ -178,22 +238,32 @@ export async function query( ); } - if (data.verifiableCredential) { - const newVerifiableCredential: NonNullable< - ParsedVerifiablePresentation["verifiableCredential"] - > = []; + if (data.verifiableCredential && Array.isArray(data.verifiableCredential)) { + const newVerifiableCredential: Promise[] = []; for (let i = 0; i < data.verifiableCredential.length; i += 100) { newVerifiableCredential.push( // Limit concurrency to avoid memory overflows. For details see // https://github.com/inrupt/solid-client-vc-js/pull/849#discussion_r1377400688 // eslint-disable-next-line no-await-in-loop ...(await Promise.all( - data.verifiableCredential - .slice(i, i + 100) - .map((vc) => verifiableCredentialToDataset(vc, { + data.verifiableCredential.slice(i, i + 100).map(async (vc: unknown) => { + if (typeof vc !== 'object' || vc === null) { + throw new Error(`Verifiable Credentail is an invalid object`); + } + + const res = await verifiableCredentialToDataset(vc, { ...options, - includeVcProperties: true - })), + includeVcProperties: options?.returnLegacyJsonld !== false, + }); + + // FIXME: Address the type issue here + if (!isRdfjsVerifiableCredential(res, namedNode(res.id))) { + throw new Error(`[${res.id}] is not a Valid Verifiable Credential`); + } + + return res; + } + ), )), ); } From 0562a674e0a55cfd71f8fefd486fd9c44840dc8e Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 25 Nov 2023 19:26:00 +0000 Subject: [PATCH 45/79] chore: add some getVerifiableCredential tests --- e2e/node/e2e.test.ts | 77 +++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index 3634d6d6..285e2d2c 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -32,6 +32,7 @@ import { getCredentialSubject, getVerifiableCredentialAllFromShape, getVerifiableCredentialApiConfiguration, + getVerifiableCredential, issueVerifiableCredential, revokeVerifiableCredential, } from "../../src/index"; @@ -267,26 +268,36 @@ describe("End-to-end verifiable credentials tests for environment", () => { vcSubject, ); - const allDeprecated = await getVerifiableCredentialAllFromShape( + const matcher = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.inrupt.com/credentials/v1.jsonld", + ], + type: ["VerifiableCredential"], + credentialSubject: { + id: vcSubject, + hasConsent: { + forPurpose: purpose, + }, + }, + } + + const [allDeprecated, allNew] = await Promise.all([getVerifiableCredentialAllFromShape( derivationService, + matcher, { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.inrupt.com/credentials/v1.jsonld", - ], - type: ["VerifiableCredential"], - credentialSubject: { - id: vcSubject, - hasConsent: { - forPurpose: purpose, - }, - }, + fetch: session.fetch, + includeExpiredVc: false, }, + ), getVerifiableCredentialAllFromShape( + derivationService, + matcher, { fetch: session.fetch, includeExpiredVc: false, + returnLegacyJsonld: false, }, - ); + )]); expect(allDeprecated).toHaveLength(2); @@ -300,28 +311,6 @@ describe("End-to-end verifiable credentials tests for environment", () => { vcSubject, ); - const allNew = await getVerifiableCredentialAllFromShape( - derivationService, - { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://schema.inrupt.com/credentials/v1.jsonld", - ], - type: ["VerifiableCredential"], - credentialSubject: { - id: vcSubject, - hasConsent: { - forPurpose: purpose, - }, - }, - }, - { - fetch: session.fetch, - includeExpiredVc: false, - returnLegacyJsonld: false, - }, - ) - expect(allNew).toHaveLength(2); // @ts-expect-error the credentialSubject property should not exist if legacy json is disabled @@ -336,7 +325,6 @@ describe("End-to-end verifiable credentials tests for environment", () => { vcSubject, ); - await expect( getVerifiableCredentialAllFromShape(derivationService, credential1, { fetch: session.fetch, @@ -363,6 +351,23 @@ describe("End-to-end verifiable credentials tests for environment", () => { }), ).resolves.toHaveLength(1); + const [credential1FetchedLegacy, credential1Fetched] = await Promise.all([getVerifiableCredential(credential1.id, { + fetch: session.fetch, + }), getVerifiableCredential(credential1.id, { + fetch: session.fetch, + returnLegacyJsonld: false + })]); + + // @ts-expect-error the credentialSubject property should not exist if legacy json is disabled + expect(credential1Fetched.credentialSubject).toBe(undefined); + expect(getCredentialSubject(credential1Fetched).value).toBe( + vcSubject, + ); + expect(credential1FetchedLegacy.credentialSubject.id).toBe(vcSubject); + expect(getCredentialSubject(credential1FetchedLegacy).value).toBe( + vcSubject, + ); + await Promise.all([ revokeVerifiableCredential(statusService, credential1.id, { fetch: session.fetch, From ced8c3ec0f925408a266dbdafb2587e956704f8f Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 26 Nov 2023 00:02:12 +0000 Subject: [PATCH 46/79] chore: fix lint errors --- e2e/node/e2e.test.ts | 71 +++++++---------- src/common/common.ts | 176 +++++++++++++++++++++--------------------- src/common/getters.ts | 7 +- src/issue/issue.ts | 1 + src/lookup/derive.ts | 3 +- src/lookup/query.ts | 57 +++++++------- 6 files changed, 147 insertions(+), 168 deletions(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index 285e2d2c..a2d77820 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -260,13 +260,9 @@ describe("End-to-end verifiable credentials tests for environment", () => { ]); expect(credential1.credentialSubject.id).toBe(vcSubject); - expect(getCredentialSubject(credential1).value).toBe( - vcSubject, - ); + expect(getCredentialSubject(credential1).value).toBe(vcSubject); expect(credential2.credentialSubject.id).toBe(vcSubject); - expect(getCredentialSubject(credential2).value).toBe( - vcSubject, - ); + expect(getCredentialSubject(credential2).value).toBe(vcSubject); const matcher = { "@context": [ @@ -280,50 +276,36 @@ describe("End-to-end verifiable credentials tests for environment", () => { forPurpose: purpose, }, }, - } + }; - const [allDeprecated, allNew] = await Promise.all([getVerifiableCredentialAllFromShape( - derivationService, - matcher, - { + const [allDeprecated, allNew] = await Promise.all([ + getVerifiableCredentialAllFromShape(derivationService, matcher, { fetch: session.fetch, includeExpiredVc: false, - }, - ), getVerifiableCredentialAllFromShape( - derivationService, - matcher, - { + }), + getVerifiableCredentialAllFromShape(derivationService, matcher, { fetch: session.fetch, includeExpiredVc: false, returnLegacyJsonld: false, - }, - )]); + }), + ]); expect(allDeprecated).toHaveLength(2); - expect(allDeprecated[0].credentialSubject.id).toBe(vcSubject); - expect(getCredentialSubject(allDeprecated[0]).value).toBe( - vcSubject, - ); + expect(getCredentialSubject(allDeprecated[0]).value).toBe(vcSubject); expect(allDeprecated[1].credentialSubject.id).toBe(vcSubject); - expect(getCredentialSubject(allDeprecated[1]).value).toBe( - vcSubject, - ); + expect(getCredentialSubject(allDeprecated[1]).value).toBe(vcSubject); expect(allNew).toHaveLength(2); - + // @ts-expect-error the credentialSubject property should not exist if legacy json is disabled - expect(allNew[0].credentialSubject).toBe(undefined); - expect(getCredentialSubject(allNew[0]).value).toBe( - vcSubject, - ); + expect(allNew[0].credentialSubject).toBeUndefined(); + expect(getCredentialSubject(allNew[0]).value).toBe(vcSubject); // @ts-expect-error the credentialSubject property should not exist if legacy json is disabled - expect(allNew[1].credentialSubject).toBe(undefined); - expect(getCredentialSubject(allNew[1]).value).toBe( - vcSubject, - ); + expect(allNew[1].credentialSubject).toBeUndefined(); + expect(getCredentialSubject(allNew[1]).value).toBe(vcSubject); await expect( getVerifiableCredentialAllFromShape(derivationService, credential1, { @@ -351,18 +333,19 @@ describe("End-to-end verifiable credentials tests for environment", () => { }), ).resolves.toHaveLength(1); - const [credential1FetchedLegacy, credential1Fetched] = await Promise.all([getVerifiableCredential(credential1.id, { - fetch: session.fetch, - }), getVerifiableCredential(credential1.id, { - fetch: session.fetch, - returnLegacyJsonld: false - })]); + const [credential1FetchedLegacy, credential1Fetched] = await Promise.all([ + getVerifiableCredential(credential1.id, { + fetch: session.fetch, + }), + getVerifiableCredential(credential1.id, { + fetch: session.fetch, + returnLegacyJsonld: false, + }), + ]); // @ts-expect-error the credentialSubject property should not exist if legacy json is disabled - expect(credential1Fetched.credentialSubject).toBe(undefined); - expect(getCredentialSubject(credential1Fetched).value).toBe( - vcSubject, - ); + expect(credential1Fetched.credentialSubject).toBeUndefined(); + expect(getCredentialSubject(credential1Fetched).value).toBe(vcSubject); expect(credential1FetchedLegacy.credentialSubject.id).toBe(vcSubject); expect(getCredentialSubject(credential1FetchedLegacy).value).toBe( vcSubject, diff --git a/src/common/common.ts b/src/common/common.ts index 94923b87..1950d037 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -445,60 +445,15 @@ export async function getVerifiableCredentialApiConfiguration( }; } -/** - * @hidden - */ -export async function verifiableCredentialToDataset< - T extends Object & { id?: string }, ->( - vc: T, - options?: ParseOptions & { - includeVcProperties: true; - }, -): Promise; -export async function verifiableCredentialToDataset< - T extends Object & { id?: string }, ->( +// eslint-disable-next-line camelcase +export function internal_applyDataset( vc: T, + store: DatasetCore, options?: ParseOptions & { includeVcProperties?: boolean; + additionalProperties?: Record; }, -): Promise; -export async function verifiableCredentialToDataset< - T extends Object & { id: string }, ->( - vc: T, - options?: ParseOptions & { - includeVcProperties?: boolean; - }, -): Promise { - let store: DatasetCore; - try { - store = await jsonLdToStore(vc, options); - } catch (e) { - throw new Error( - `Parsing the Verifiable Credential as JSON-LD failed: ${e}`, - ); - } - - if (typeof vc.id !== "string") { - throw new Error( - `Expected vc.id to be a string, found [${ - vc.id - }] of type [${typeof vc.id}] on ${JSON.stringify(vc, null, 2)}`, - ); - } - - return internal_applyDataset(vc, store, options) -} - -export function internal_applyDataset( -vc: T, -store: DatasetCore, -options?: ParseOptions & { - includeVcProperties?: boolean; - additionalProperties?: Record; -}): DatasetWithId { +): DatasetWithId { return Object.freeze({ id: vc.id, ...(options?.includeVcProperties && vc), @@ -532,60 +487,47 @@ options?: ParseOptions & { } /** - * Dereference a VC URL, and verify that the resulting content is valid. - * - * @param vcUrl The URL of the VC. - * @param options Options to customize the function behavior. - * - options.fetch: Specify a WHATWG-compatible authenticated fetch. - * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response - * @returns The dereferenced VC if valid. Throws otherwise. - * @since 0.4.0 - * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future - * versions of this library. + * @hidden */ -export async function getVerifiableCredential( - vcUrl: UrlString, +export async function verifiableCredentialToDataset( + vc: T, options?: ParseOptions & { - fetch?: typeof fetch; - returnLegacyJsonld?: true; + includeVcProperties: true; }, -): Promise; -/** - * Dereference a VC URL, and verify that the resulting content is valid. - * - * @param vcUrl The URL of the VC. - * @param options Options to customize the function behavior. - * - options.fetch: Specify a WHATWG-compatible authenticated fetch. - * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response - * @returns The dereferenced VC if valid. Throws otherwise. - * @since 0.4.0 - */ -export async function getVerifiableCredential( - vcUrl: UrlString, +): Promise; +export async function verifiableCredentialToDataset( + vc: T, options?: ParseOptions & { - fetch?: typeof fetch; - returnLegacyJsonld?: boolean; + includeVcProperties?: boolean; }, ): Promise; -export async function getVerifiableCredential( - vcUrl: UrlString, +export async function verifiableCredentialToDataset( + vc: T, options?: ParseOptions & { - fetch?: typeof fetch; - returnLegacyJsonld?: boolean; + includeVcProperties?: boolean; }, ): Promise { - const authFetch = options?.fetch ?? uniFetch; - const response = await authFetch(vcUrl); + let store: DatasetCore; + try { + store = await jsonLdToStore(vc, options); + } catch (e) { + throw new Error( + `Parsing the Verifiable Credential as JSON-LD failed: ${e}`, + ); + } - if (!response.ok) { + if (typeof vc.id !== "string") { throw new Error( - `Fetching the Verifiable Credential [${vcUrl}] failed: ${response.status} ${response.statusText}`, + `Expected vc.id to be a string, found [${ + vc.id + }] of type [${typeof vc.id}] on ${JSON.stringify(vc, null, 2)}`, ); } - return internal_getVerifiableCredentialFromResponse(vcUrl, response, options); + return internal_applyDataset(vc as { id: string }, store, options); } +// eslint-disable-next-line camelcase export async function internal_getVerifiableCredentialFromResponse( vcUrl: UrlString | undefined, response: Response, @@ -601,7 +543,7 @@ export async function internal_getVerifiableCredentialFromResponse( }, ): Promise; export async function internal_getVerifiableCredentialFromResponse( - vcUrl: UrlString | undefined, + vcUrlInput: UrlString | undefined, response: Response, options?: ParseOptions & { returnLegacyJsonld?: boolean; @@ -609,6 +551,7 @@ export async function internal_getVerifiableCredentialFromResponse( ): Promise { const returnLegacy = options?.returnLegacyJsonld !== false; let vc: unknown | VerifiableCredentialBase; + let vcUrl = vcUrlInput; try { vc = await response.json(); @@ -667,3 +610,58 @@ export async function internal_getVerifiableCredentialFromResponse( } return parsedVc; } + +/** + * Dereference a VC URL, and verify that the resulting content is valid. + * + * @param vcUrl The URL of the VC. + * @param options Options to customize the function behavior. + * - options.fetch: Specify a WHATWG-compatible authenticated fetch. + * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response + * @returns The dereferenced VC if valid. Throws otherwise. + * @since 0.4.0 + * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future + * versions of this library. + */ +export async function getVerifiableCredential( + vcUrl: UrlString, + options?: ParseOptions & { + fetch?: typeof fetch; + returnLegacyJsonld?: true; + }, +): Promise; +/** + * Dereference a VC URL, and verify that the resulting content is valid. + * + * @param vcUrl The URL of the VC. + * @param options Options to customize the function behavior. + * - options.fetch: Specify a WHATWG-compatible authenticated fetch. + * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response + * @returns The dereferenced VC if valid. Throws otherwise. + * @since 0.4.0 + */ +export async function getVerifiableCredential( + vcUrl: UrlString, + options?: ParseOptions & { + fetch?: typeof fetch; + returnLegacyJsonld?: boolean; + }, +): Promise; +export async function getVerifiableCredential( + vcUrl: UrlString, + options?: ParseOptions & { + fetch?: typeof fetch; + returnLegacyJsonld?: boolean; + }, +): Promise { + const authFetch = options?.fetch ?? uniFetch; + const response = await authFetch(vcUrl); + + if (!response.ok) { + throw new Error( + `Fetching the Verifiable Credential [${vcUrl}] failed: ${response.status} ${response.statusText}`, + ); + } + + return internal_getVerifiableCredentialFromResponse(vcUrl, response, options); +} diff --git a/src/common/getters.ts b/src/common/getters.ts index 6caa888a..4bba0827 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -18,12 +18,7 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type { - BlankNode, - DatasetCore, - Literal, - NamedNode -} from "@rdfjs/types"; +import type { BlankNode, DatasetCore, Literal, NamedNode } from "@rdfjs/types"; import { DataFactory } from "n3"; import { isUrl } from "./common"; import { cred, dc, rdf, sec, xsd } from "./constants"; diff --git a/src/issue/issue.ts b/src/issue/issue.ts index 75876f98..93b8a585 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -30,6 +30,7 @@ import { concatenateContexts, defaultContext, defaultCredentialTypes, + // eslint-disable-next-line camelcase internal_getVerifiableCredentialFromResponse, } from "../common/common"; import type { ParseOptions } from "../parser/jsonld"; diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index bc902ef4..0939212b 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -20,7 +20,6 @@ // import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; -import { DataFactory } from "n3"; import type { Iri, VerifiableCredential, @@ -167,7 +166,7 @@ export async function getVerifiableCredentialAllFromShape( if (options?.returnLegacyJsonld === false) { const vp = await query(holderEndpoint, vpRequest, { fetch: options?.fetch ?? fallbackFetch, - returnLegacyJsonld: false + returnLegacyJsonld: false, }); return vp.verifiableCredential ?? []; } diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 51a1ab16..74d58abc 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -21,6 +21,7 @@ import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; import { DataFactory } from "n3"; +import { isRdfjsVerifiableCredential } from ".."; import type { Iri, VerifiableCredential, @@ -28,17 +29,14 @@ import type { VerifiablePresentation, } from "../common/common"; import { - internal_applyDataset, isVerifiablePresentation, normalizeVp, verifiableCredentialToDataset, } from "../common/common"; -import { jsonLdToStore, type ParseOptions } from "../parser/jsonld"; import type { DatasetWithId } from "../common/getters"; -import { isRdfjsVerifiableCredential, isRdfjsVerifiablePresentation } from ".."; -import { cred, rdf } from "../common/constants"; +import { type ParseOptions } from "../parser/jsonld"; -const { namedNode, defaultGraph } = DataFactory; +const { namedNode } = DataFactory; /** * Based on https://w3c-ccg.github.io/vp-request-spec/#query-by-example. @@ -140,8 +138,7 @@ export async function query( returnLegacyJsonld?: boolean; }>, ): Promise< - | ParsedVerifiablePresentation - | { verifiableCredential?: DatasetWithId[] } + ParsedVerifiablePresentation | { verifiableCredential?: DatasetWithId[] } > { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { @@ -185,7 +182,7 @@ export async function query( // )}`, // ); // } - + // // In the future we want to get rid of this and get the verifiableCredential ids from the store // // the reason we need this for now is because we need the verifiableCredential JSON object for // // the toJSON method. @@ -199,7 +196,7 @@ export async function query( // } // const c = internal_applyDataset(vc as { id: string }, store, options) - + // if (!isRdfjsVerifiableCredential(store, namedNode(c.id))) { // throw new Error(`[${c.id}] is not a valid Verifiable Credential`); // } @@ -220,7 +217,7 @@ export async function query( // All code below here should is deprecated let data; try { - data = await response.json() + data = await response.json(); if (options?.returnLegacyJsonld !== false) { data = normalizeVp(data); @@ -230,7 +227,10 @@ export async function query( `The holder [${queryEndpoint}] did not return a valid JSON response: parsing failed with error ${e}`, ); } - if (options?.returnLegacyJsonld !== false && !isVerifiablePresentation(data)) { + if ( + options?.returnLegacyJsonld !== false && + !isVerifiablePresentation(data) + ) { throw new Error( `The holder [${queryEndpoint}] did not return a Verifiable Presentation: ${JSON.stringify( data, @@ -246,24 +246,27 @@ export async function query( // https://github.com/inrupt/solid-client-vc-js/pull/849#discussion_r1377400688 // eslint-disable-next-line no-await-in-loop ...(await Promise.all( - data.verifiableCredential.slice(i, i + 100).map(async (vc: unknown) => { - if (typeof vc !== 'object' || vc === null) { - throw new Error(`Verifiable Credentail is an invalid object`); - } - - const res = await verifiableCredentialToDataset(vc, { - ...options, - includeVcProperties: options?.returnLegacyJsonld !== false, - }); + data.verifiableCredential + .slice(i, i + 100) + .map(async (vc: unknown) => { + if (typeof vc !== "object" || vc === null) { + throw new Error(`Verifiable Credentail is an invalid object`); + } + + const res = await verifiableCredentialToDataset(vc, { + ...options, + includeVcProperties: options?.returnLegacyJsonld !== false, + }); - // FIXME: Address the type issue here - if (!isRdfjsVerifiableCredential(res, namedNode(res.id))) { - throw new Error(`[${res.id}] is not a Valid Verifiable Credential`); - } + // FIXME: Address the type issue here + if (!isRdfjsVerifiableCredential(res, namedNode(res.id))) { + throw new Error( + `[${res.id}] is not a Valid Verifiable Credential`, + ); + } - return res; - } - ), + return res; + }), )), ); } From bd6e8653703a74e4dad5940beefd23c093d27836 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 26 Nov 2023 01:24:04 +0000 Subject: [PATCH 47/79] chore: improve test coverage --- src/common/common.test.ts | 571 ++++++++++++++++++++++++++++---------- src/lookup/derive.test.ts | 145 +++++++++- 2 files changed, 569 insertions(+), 147 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 13e8a9d3..faeaad78 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -464,83 +464,204 @@ describe("concatenateContexts", () => { }); describe("getVerifiableCredential", () => { - it("defaults to an unauthenticated fetch", async () => { - const mockedFetchModule = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; - mockedFetchModule.fetch.mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultCredential()), { - headers: new Headers([["content-type", "application/json"]]), - }), - ); - - const redirectUrl = new URL("https://redirect.url"); - redirectUrl.searchParams.append( - "requestVcUrl", - encodeURI("https://some.vc"), - ); - redirectUrl.searchParams.append( - "redirectUrl", - encodeURI("https://requestor.redirect.url"), - ); + describe("defaults to an unauthenticated fetch", () => { + let mockedFetchModule: jest.Mocked; - await getVerifiableCredential("https://some.vc"); - expect(mockedFetchModule.fetch).toHaveBeenCalledWith("https://some.vc"); - }); - - it("uses the provided fetch if any", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + beforeEach(() => { + mockedFetchModule = jest.requireMock( + "@inrupt/universal-fetch", + ) as jest.Mocked; + mockedFetchModule.fetch.mockResolvedValueOnce( new Response(JSON.stringify(mockDefaultCredential()), { headers: new Headers([["content-type", "application/json"]]), }), ); - await getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + const redirectUrl = new URL("https://redirect.url"); + redirectUrl.searchParams.append( + "requestVcUrl", + encodeURI("https://example.org/ns/someCredentialInstance"), + ); + redirectUrl.searchParams.append( + "redirectUrl", + encodeURI("https://requestor.redirect.url"), + ); }); - expect(mockedFetch).toHaveBeenCalledWith("https://some.vc"); - }); - it("throws if the VC ID cannot be dereferenced", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(undefined, { status: 401, statusText: "Unauthenticated" }), + it("returnLegacyJsonld: true", async () => { + await getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + ); + expect(mockedFetchModule.fetch).toHaveBeenCalledWith( + "https://example.org/ns/someCredentialInstance", + ); + }); + it("returnLegacyJsonld: false", async () => { + await getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + returnLegacyJsonld: false, + }, ); + expect(mockedFetchModule.fetch).toHaveBeenCalledWith( + "https://example.org/ns/someCredentialInstance", + ); + }); + }); - await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow(/https:\/\/some.vc.*401.*Unauthenticated/); + describe("uses the provided fetch if any", () => { + let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + + beforeEach(() => { + mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mockDefaultCredential()), { + headers: new Headers([["content-type", "application/json"]]), + }), + ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + }); + + it("returnLegacyJsonld: true", async () => { + await getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + }, + ); + expect(mockedFetch).toHaveBeenCalledWith( + "https://example.org/ns/someCredentialInstance", + ); + }); + it("returnLegacyJsonld: false", async () => { + await getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ); + expect(mockedFetch).toHaveBeenCalledWith( + "https://example.org/ns/someCredentialInstance", + ); + }); }); - it("throws if the dereferenced data is invalid JSON", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce(new Response("Not JSON.")); + describe("throws if the VC ID cannot be dereferenced", () => { + let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; - await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow(/https:\/\/some.vc.*JSON/); + beforeEach(() => { + mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(undefined, { + status: 401, + statusText: "Unauthenticated", + }), + ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + }); + + it("returnLegacyJsonld: true", async () => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + }, + ), + ).rejects.toThrow( + "Fetching the Verifiable Credential [https://example.org/ns/someCredentialInstance] failed: 401 Unauthenticated", + ); + }); + it("returnLegacyJsonld: false", async () => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ), + ).rejects.toThrow( + "Fetching the Verifiable Credential [https://example.org/ns/someCredentialInstance] failed: 401 Unauthenticated", + ); + }); }); - it("throws if the dereferenced data is not a VC", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify({ something: "but not a VC" })), + describe("throws if the dereferenced data is invalid JSON", () => { + let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + + beforeEach(() => { + mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce(new Response("Not JSON")) as jest.MockedFunction< + (typeof UniversalFetch)["fetch"] + >; + }); + + it("returnLegacyJsonld: true", async () => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + }, + ), + ).rejects.toThrow( + "Parsing the Verifiable Credential [https://example.org/ns/someCredentialInstance] as JSON failed: SyntaxError: Unexpected token 'N', \"Not JSON\" is not valid JSON", ); + }); + it("returnLegacyJsonld: false", async () => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ), + ).rejects.toThrow( + "Parsing the Verifiable Credential [https://example.org/ns/someCredentialInstance] as JSON failed: SyntaxError: Unexpected token 'N', \"Not JSON\" is not valid JSON", + ); + }); + }); - await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow(/https:\/\/some.vc.*Verifiable Credential/); + describe("throws if the dereferenced data is not a VC", () => { + let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + + beforeEach(() => { + mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify({ something: "but not a VC" })), + ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + }); + + it("returnLegacyJsonld: true", async () => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + }, + ), + ).rejects.toThrow( + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + }); + it("returnLegacyJsonld: false", async () => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ), + ).rejects.toThrow( + "Verifiable credential is not an object, or does not have an id", + ); + }); }); it.skip("throws if the dereferenced data has an unsupported content type", async () => { @@ -558,27 +679,33 @@ describe("getVerifiableCredential", () => { }); it("throws if the dereferenced data is empty", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify({}), { headers: new Headers([["content-type", "application/json"]]), }), - ); + ); await expect( - getVerifiableCredential("https://some.vc", { + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + await expect( + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }), + ).rejects.toThrow( + "Verifiable credential is not an object, or does not have an id", ); }); it("throws if the vc is a blank node", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response( JSON.stringify({ "@type": "https://www.w3.org/2018/credentials#VerifiableCredential", @@ -587,7 +714,7 @@ describe("getVerifiableCredential", () => { headers: new Headers([["content-type", "application/json"]]), }, ), - ); + ); await expect( getVerifiableCredential("https://some.vc", { @@ -596,12 +723,19 @@ describe("getVerifiableCredential", () => { ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", ); + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }), + ).rejects.toThrow( + "Verifiable credential is not an object, or does not have an id", + ); }); it("throws if the vc has a type that is a literal", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response( JSON.stringify({ "@context": "https://www.w3.org/2018/credentials/v1", @@ -618,7 +752,7 @@ describe("getVerifiableCredential", () => { headers: new Headers([["content-type", "application/json"]]), }, ), - ); + ); await expect( getVerifiableCredential("https://some.vc", { @@ -627,12 +761,18 @@ describe("getVerifiableCredential", () => { ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", ); + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential", + ); }); it("throws if the dereferenced data has 2 vcs", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response( JSON.stringify([ mockDefaultCredential(), @@ -642,7 +782,7 @@ describe("getVerifiableCredential", () => { headers: new Headers([["content-type", "application/json"]]), }, ), - ); + ); await expect( getVerifiableCredential("https://some.vc", { @@ -651,16 +791,23 @@ describe("getVerifiableCredential", () => { ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", ); + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }), + ).rejects.toThrow( + "Verifiable credential is not an object, or does not have an id", + ); }); it("throws if the dereferenced data has 2 proofs", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify(mockDefaultCredential2Proofs()), { headers: new Headers([["content-type", "application/json"]]), }), - ); + ); await expect( getVerifiableCredential("https://some.vc", { @@ -669,26 +816,41 @@ describe("getVerifiableCredential", () => { ).rejects.toThrow( "The value received from [https://some.vc] is not a Verifiable Credential", ); + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }), + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential", + ); }); it("throws if the date field is not a valid xsd:dateTime", async () => { const mocked = mockDefaultCredential(); mocked.issuanceDate = "http://example.org/not/a/date"; - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), }), - ); + ); await expect( - getVerifiableCredential("https://some.vc", { + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + await expect( + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }), + ).rejects.toThrow( + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", ); }); @@ -699,20 +861,27 @@ describe("getVerifiableCredential", () => { mocked["https://www.w3.org/2018/credentials#issuanceDate"] = "http://example.org/not/a/date"; - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), }), - ); + ); await expect( - getVerifiableCredential("https://some.vc", { + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + await expect( + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }), + ).rejects.toThrow( + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", ); }); @@ -724,20 +893,27 @@ describe("getVerifiableCredential", () => { "@id": "http://example.org/not/a/date", }; - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), }), - ); + ); await expect( - getVerifiableCredential("https://some.vc", { + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + await expect( + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }), + ).rejects.toThrow( + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", ); }); @@ -746,20 +922,27 @@ describe("getVerifiableCredential", () => { // @ts-expect-error issuer is of type string on the VC type mocked.issuer = { "@value": "my string" }; - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), }), - ); + ); await expect( - getVerifiableCredential("https://some.vc", { + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + await expect( + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }), + ).rejects.toThrow( + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", ); }); @@ -776,24 +959,36 @@ describe("getVerifiableCredential", () => { }, }; - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), }), - ); + ); - const vc = await getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }); + const vc = await getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + }, + ); + const vcNoLegacy = await getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ); // Since we have dataset properties in vc it should match the result // but won't equal expect(vc).toMatchObject(mocked); + expect(vcNoLegacy).toMatchObject({ id: mocked.id }); + expect(vcNoLegacy).not.toMatchObject(mocked); // However we DO NOT want these properties showing up when we stringify // the VC expect(JSON.parse(JSON.stringify(vc))).toEqual(mocked); + expect(JSON.parse(JSON.stringify(vcNoLegacy))).toEqual(mocked); }); it("should handle credential subjects with multiple objects and a custom context", async () => { @@ -815,24 +1010,37 @@ describe("getVerifiableCredential", () => { }, }; - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), }), - ); + ); - const vc = await getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }); + const vc = await getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + }, + ); + + const vcNoLegacy = await getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ); // Since we have dataset properties in vc it should match the result // but won't equal expect(vc).toMatchObject(mocked); + expect(vcNoLegacy).toMatchObject({ id: mocked.id }); + expect(vcNoLegacy).not.toMatchObject(mocked); // However we DO NOT want these properties showing up when we stringify // the VC expect(JSON.parse(JSON.stringify(vc))).toEqual(mocked); + expect(JSON.parse(JSON.stringify(vcNoLegacy))).toEqual(mocked); }); it("throws if there are 2 proof values", async () => { @@ -840,31 +1048,37 @@ describe("getVerifiableCredential", () => { // @ts-expect-error proofValue is a string not string[] in VC type mocked.proof.proofValue = [mocked.proof.proofValue, "abc"]; - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), }), - ); + ); await expect( - getVerifiableCredential("https://some.vc", { + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], }), ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + await expect( + getVerifiableCredential("https://example.org/ns/someCredentialInstance", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }), + ).rejects.toThrow( + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", ); }); it("returns the fetched VC and the redirect URL", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify(mockDefaultCredential()), { headers: new Headers([["content-type", "application/json"]]), }), - ); + ); const vc = await getVerifiableCredential("https://some.vc", { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], @@ -929,9 +1143,26 @@ describe("getVerifiableCredential", () => { it("errors if the context contains an IRI that is not cached", async () => { await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + }, + ), + ).rejects.toThrow( + "Unexpected context requested [http://example.org/my/sample/context]", + ); + }); + + it("errors if the context contains an IRI that is not cached [returnLegacyJsonld: false]", async () => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ), ).rejects.toThrow( "Unexpected context requested [http://example.org/my/sample/context]", ); @@ -950,25 +1181,77 @@ describe("getVerifiableCredential", () => { ); await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - allowContextFetching: true, - }), + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + allowContextFetching: true, + }, + ), ).resolves.toMatchObject(mockCredential); }); + it("resolves if allowContextFetching is enabled and the context can be fetched [returnLegacyJsonld: false]", async () => { + (uniFetch as jest.Mock).mockResolvedValueOnce( + new Response( + JSON.stringify({ + "@context": {}, + }), + { + headers: new Headers([["content-type", "application/ld+json"]]), + }, + ), + ); + + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + allowContextFetching: true, + returnLegacyJsonld: false, + }, + ), + ).resolves.toMatchObject({ + id: "https://example.org/ns/someCredentialInstance", + }); + }); + it("resolves if the context is cached", async () => { await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - allowContextFetching: true, - contexts: { - "http://example.org/my/sample/context": { - "@context": {}, + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + allowContextFetching: true, + contexts: { + "http://example.org/my/sample/context": { + "@context": {}, + }, }, }, - }), + ), ).resolves.toMatchObject(mockCredential); }); + + it("resolves if the context is cached [returnLegacyJsonld: false]", async () => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + allowContextFetching: true, + returnLegacyJsonld: false, + contexts: { + "http://example.org/my/sample/context": { + "@context": {}, + }, + }, + }, + ), + ).resolves.toMatchObject({ + id: "https://example.org/ns/someCredentialInstance", + }); + }); }); }); diff --git a/src/lookup/derive.test.ts b/src/lookup/derive.test.ts index 47813ec3..15ddc1fd 100644 --- a/src/lookup/derive.test.ts +++ b/src/lookup/derive.test.ts @@ -28,6 +28,7 @@ import defaultGetVerifilableCredentialAllFromShape, { } from "./derive"; import type * as QueryModule from "./query"; import type { VerifiableCredential } from "../common/common"; +import { getCredentialSubject } from "../common/getters"; jest.mock("@inrupt/universal-fetch", () => { const fetchModule = jest.requireActual( @@ -69,6 +70,24 @@ describe("getVerifiableCredentialAllFromShape", () => { } catch (_e) {} expect(mockedFetch).toHaveBeenCalled(); }); + it("uses the provided fetch if any [returnLegacyJsonld: false]", async () => { + const mockedFetch = jest.fn() as typeof fetch; + try { + await getVerifiableCredentialAllFromShape( + "https://some.endpoint", + { + "@context": ["https://some.context"], + credentialSubject: { id: "https://some.subject/" }, + }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ); + // eslint-disable-next-line no-empty + } catch (_e) {} + expect(mockedFetch).toHaveBeenCalled(); + }); it("defaults to an unauthenticated fetch if no fetch is provided", async () => { const mockedFetch = jest.requireMock( @@ -82,6 +101,24 @@ describe("getVerifiableCredentialAllFromShape", () => { expect(mockedFetch.fetch).toHaveBeenCalled(); }); + it("defaults to an unauthenticated fetch if no fetch is provided [returnLegacyJsonld: false]", async () => { + const mockedFetch = jest.requireMock( + "@inrupt/universal-fetch", + ) as jest.Mocked; + mockedFetch.fetch.mockResolvedValue(mockDeriveEndpointDefaultResponse()); + await getVerifiableCredentialAllFromShape( + "https://some.endpoint", + { + "@context": ["https://some.context"], + credentialSubject: { id: "https://some.subject/" }, + }, + { + returnLegacyJsonld: false, + }, + ); + expect(mockedFetch.fetch).toHaveBeenCalled(); + }); + it("includes the expired VC options if requested", async () => { const mockedFetch = jest .fn<(typeof UniversalFetch)["fetch"]>() @@ -105,6 +142,30 @@ describe("getVerifiableCredentialAllFromShape", () => { ); }); + it("includes the expired VC options if requested [returnLegacyJsonld: false]", async () => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValue(mockDeriveEndpointDefaultResponse()); + await getVerifiableCredentialAllFromShape( + "https://some.endpoint", + { + "@context": ["https://some.context"], + credentialSubject: { id: "https://some.subject/" }, + }, + { + includeExpiredVc: true, + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ); + expect(mockedFetch).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + body: expect.stringContaining("ExpiredVerifiableCredential"), + }), + ); + }); + it("builds a legacy VP request from the provided VC shape", async () => { const mockedFetch = jest .fn<(typeof UniversalFetch)["fetch"]>() @@ -134,6 +195,38 @@ describe("getVerifiableCredentialAllFromShape", () => { ); }); + it("builds a legacy VP request from the provided VC shape [returnLegacyJsonld: false]", async () => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValue(mockDeriveEndpointDefaultResponse()); + const queryModule = jest.requireActual("./query") as typeof QueryModule; + const spiedQuery = jest.spyOn(queryModule, "query"); + await getVerifiableCredentialAllFromShape( + "https://some.endpoint", + { + "@context": ["https://some.context"], + credentialSubject: { id: "https://some.subject/" }, + }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ); + expect(spiedQuery).toHaveBeenCalledWith( + "https://some.endpoint", + expect.objectContaining({ + verifiableCredential: { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://some.context", + ], + credentialSubject: { id: "https://some.subject/" }, + }, + }), + expect.anything(), + ); + }); + it("returns the VCs from the obtained VP on a successful response", async () => { const mockedFetch = jest .fn<(typeof UniversalFetch)["fetch"]>() @@ -158,10 +251,43 @@ describe("getVerifiableCredentialAllFromShape", () => { ); }); - it("returns an empty array if the VP contains no VCs", async () => { + it("returns the VCs from the obtained VP on a successful response [returnLegacyJsonld: false]", async () => { const mockedFetch = jest .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue( + .mockResolvedValue(mockDeriveEndpointDefaultResponse()); + + const vc = await getVerifiableCredentialAllFromShape( + "https://some.endpoint", + { + "@context": ["https://some.context"], + credentialSubject: { id: "https://some.webid.provider/strelka" }, + }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ); + + expect(vc).toMatchObject( + mockDefaultPresentation().verifiableCredential!.map((v) => ({ + id: v.id, + })), + ); + + expect(vc.map((v) => getCredentialSubject(v).value)).toEqual( + mockDefaultPresentation().verifiableCredential?.map( + () => "https://some.webid.provider/strelka", + ), + ); + + expect(JSON.parse(JSON.stringify(vc))).toEqual( + mockDefaultPresentation().verifiableCredential, + ); + }); + + it("returns an empty array if the VP contains no VCs", async () => { + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response( JSON.stringify({ ...mockDefaultPresentation(), @@ -172,7 +298,7 @@ describe("getVerifiableCredentialAllFromShape", () => { statusText: "OK", }, ), - ); + ); await expect( getVerifiableCredentialAllFromShape( "https://some.endpoint", @@ -183,6 +309,19 @@ describe("getVerifiableCredentialAllFromShape", () => { { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, ), ).resolves.toEqual([]); + await expect( + getVerifiableCredentialAllFromShape( + "https://some.endpoint", + { + "@context": ["https://some.context"], + credentialSubject: { id: "https://some.subject/" }, + }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ), + ).resolves.toEqual([]); }); }); From 369b698f05da6e2ed9ce7d982b59d4df61fb48e8 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 26 Nov 2023 01:43:01 +0000 Subject: [PATCH 48/79] chore: extend test suite --- src/lookup/derive.test.ts | 1 + src/lookup/derive.ts | 16 +----- src/lookup/query.test.ts | 112 ++++++++++++++++++++++++++++---------- src/lookup/query.ts | 11 ++++ 4 files changed, 97 insertions(+), 43 deletions(-) diff --git a/src/lookup/derive.test.ts b/src/lookup/derive.test.ts index 15ddc1fd..740d6e67 100644 --- a/src/lookup/derive.test.ts +++ b/src/lookup/derive.test.ts @@ -70,6 +70,7 @@ describe("getVerifiableCredentialAllFromShape", () => { } catch (_e) {} expect(mockedFetch).toHaveBeenCalled(); }); + it("uses the provided fetch if any [returnLegacyJsonld: false]", async () => { const mockedFetch = jest.fn() as typeof fetch; try { diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index 0939212b..57eebaa0 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -146,10 +146,7 @@ export async function getVerifiableCredentialAllFromShape( returnLegacyJsonld?: boolean; }>, ): Promise { - const internalOptions = { ...options }; - if (internalOptions.fetch === undefined) { - internalOptions.fetch = fallbackFetch; - } + const fetchFn = options?.fetch ?? fallbackFetch; // The request payload depends on the target endpoint. const vpRequest = holderEndpoint.endsWith("/query") ? // The target endpoint is spec-compliant, and uses a standard VP request. @@ -163,16 +160,9 @@ export async function getVerifiableCredentialAllFromShape( // The legacy proprietary format is casted as a VP request to be passed to the `query` function. ) as unknown as VerifiablePresentationRequest); - if (options?.returnLegacyJsonld === false) { - const vp = await query(holderEndpoint, vpRequest, { - fetch: options?.fetch ?? fallbackFetch, - returnLegacyJsonld: false, - }); - return vp.verifiableCredential ?? []; - } - const vp = await query(holderEndpoint, vpRequest, { - fetch: options?.fetch ?? fallbackFetch, + fetch: fetchFn, + returnLegacyJsonld: options?.returnLegacyJsonld, }); return vp?.verifiableCredential ?? []; } diff --git a/src/lookup/query.test.ts b/src/lookup/query.test.ts index c3580dc6..67b39a53 100644 --- a/src/lookup/query.test.ts +++ b/src/lookup/query.test.ts @@ -19,7 +19,7 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import { jest, it, describe, expect } from "@jest/globals"; +import { jest, it, describe, expect, beforeEach } from "@jest/globals"; import { Response } from "@inrupt/universal-fetch"; import type * as UniversalFetch from "@inrupt/universal-fetch"; import type { QueryByExample } from "./query"; @@ -53,20 +53,37 @@ const mockRequest: QueryByExample = { describe("query", () => { describe("by example", () => { - it("uses the provided fetch if any", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultPresentation()), { - status: 200, - }), + describe("uses the provided fetch if any", () => { + let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + + beforeEach(() => { + mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response(JSON.stringify(mockDefaultPresentation()), { + status: 200, + }), + ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + }); + + it("returnLegacyJsonld: true", async () => { + await query( + "https://some.endpoint/query", + { query: [mockRequest] }, + { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, ); - await query( - "https://some.endpoint/query", - { query: [mockRequest] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, - ); - expect(mockedFetch).toHaveBeenCalled(); + expect(mockedFetch).toHaveBeenCalled(); + }); + it("returnLegacyJsonld: false", async () => { + await query( + "https://some.endpoint/query", + { query: [mockRequest] }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ); + expect(mockedFetch).toHaveBeenCalled(); + }); }); it("defaults to an unauthenticated fetch if no fetch is provided", async () => { @@ -83,13 +100,12 @@ describe("query", () => { }); it("throws if the given endpoint returns an error", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(undefined, { status: 404, }), - ); + ); await expect(() => query( "https://example.org/query", @@ -97,16 +113,25 @@ describe("query", () => { { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, ), ).rejects.toThrow(); + await expect(() => + query( + "https://example.org/query", + { query: [mockRequest] }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ), + ).rejects.toThrow(); }); it("throws if the endpoint responds with a non-JSON payload", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response("Not JSON", { status: 200, }), - ); + ); await expect(() => query( "https://example.org/query", @@ -114,16 +139,25 @@ describe("query", () => { { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, ), ).rejects.toThrow(); + await expect(() => + query( + "https://example.org/query", + { query: [mockRequest] }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ), + ).rejects.toThrow(); }); it("throws if the endpoint responds with a non-VP payload", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify({ json: "but not a VP" }), { status: 200, }), - ); + ); await expect(() => query( "https://example.org/query", @@ -131,6 +165,14 @@ describe("query", () => { { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, ), ).rejects.toThrow(); + // FIXME: Re-enable this + // await expect(() => + // query( + // "https://example.org/query", + // { query: [mockRequest] }, + // { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], returnLegacyJsonld: false }, + // ), + // ).rejects.toThrow(); }); it("posts a request with the appropriate media type", async () => { @@ -158,21 +200,31 @@ describe("query", () => { }); it("returns the VP sent by the endpoint", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => new Response(JSON.stringify(mockDefaultPresentation()), { status: 200, }), - ); + ); const vp = await query( "https://example.org/query", { query: [mockRequest] }, { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, ); + const vpNoLegacy = await query( + "https://example.org/query", + { query: [mockRequest] }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ); expect(vp).toMatchObject(mockDefaultPresentation()); expect(JSON.parse(JSON.stringify(vp))).toEqual(mockDefaultPresentation()); + expect(JSON.parse(JSON.stringify(vpNoLegacy))).toEqual( + mockDefaultPresentation(), + ); }); it("normalizes the VP sent by the endpoint", async () => { diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 74d58abc..7fa15209 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -129,6 +129,17 @@ export async function query( returnLegacyJsonld: false; }>, ): Promise<{ verifiableCredential?: DatasetWithId[] }>; +export async function query( + queryEndpoint: Iri, + vpRequest: VerifiablePresentationRequest, + options?: ParseOptions & + Partial<{ + fetch: typeof fallbackFetch; + returnLegacyJsonld?: boolean; + }>, +): Promise< + ParsedVerifiablePresentation | { verifiableCredential?: DatasetWithId[] } +>; export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, From 349b3610419a415fa4439b08d15f31fa48324115 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 26 Nov 2023 11:22:41 +0000 Subject: [PATCH 49/79] chore: remove json error details from test --- src/common/common.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index faeaad78..0097db25 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -608,7 +608,7 @@ describe("getVerifiableCredential", () => { }, ), ).rejects.toThrow( - "Parsing the Verifiable Credential [https://example.org/ns/someCredentialInstance] as JSON failed: SyntaxError: Unexpected token 'N', \"Not JSON\" is not valid JSON", + "Parsing the Verifiable Credential [https://example.org/ns/someCredentialInstance] as JSON failed: SyntaxError:", ); }); it("returnLegacyJsonld: false", async () => { @@ -621,7 +621,7 @@ describe("getVerifiableCredential", () => { }, ), ).rejects.toThrow( - "Parsing the Verifiable Credential [https://example.org/ns/someCredentialInstance] as JSON failed: SyntaxError: Unexpected token 'N', \"Not JSON\" is not valid JSON", + "Parsing the Verifiable Credential [https://example.org/ns/someCredentialInstance] as JSON failed: SyntaxError:", ); }); }); From 5119f18a984e8ad0cc6f9f3271252a0523747853 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 26 Nov 2023 11:49:52 +0000 Subject: [PATCH 50/79] chore: get full test coverage --- src/common/common.mock.ts | 2 +- src/lookup/derive.ts | 2 +- src/lookup/query.test.ts | 92 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/common/common.mock.ts b/src/common/common.mock.ts index 6ee65c4c..c344d835 100644 --- a/src/common/common.mock.ts +++ b/src/common/common.mock.ts @@ -137,7 +137,7 @@ export const mockDefaultCredential2Proofs = ( }; export const mockPartialPresentation = ( - credentials: VerifiableCredentialBase[], + credentials?: VerifiableCredentialBase[], claims?: Partial, ): Record => { return { diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index 57eebaa0..92a9c03b 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -164,7 +164,7 @@ export async function getVerifiableCredentialAllFromShape( fetch: fetchFn, returnLegacyJsonld: options?.returnLegacyJsonld, }); - return vp?.verifiableCredential ?? []; + return vp.verifiableCredential ?? []; } export default getVerifiableCredentialAllFromShape; diff --git a/src/lookup/query.test.ts b/src/lookup/query.test.ts index 67b39a53..63273d55 100644 --- a/src/lookup/query.test.ts +++ b/src/lookup/query.test.ts @@ -25,8 +25,10 @@ import type * as UniversalFetch from "@inrupt/universal-fetch"; import type { QueryByExample } from "./query"; import { query } from "./query"; import { + defaultVerifiableClaims, mockDefaultCredential, mockDefaultPresentation, + mockPartialPresentation, } from "../common/common.mock"; jest.mock("@inrupt/universal-fetch", () => { @@ -86,6 +88,96 @@ describe("query", () => { }); }); + describe("resolves when the VP contains no VCs", () => { + let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + + beforeEach(() => { + mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response( + JSON.stringify( + mockPartialPresentation(undefined, defaultVerifiableClaims), + ), + { + status: 200, + }, + ), + ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + }); + + it("returnLegacyJsonld: true", async () => { + await expect( + query( + "https://some.endpoint/query", + { query: [mockRequest] }, + { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + ), + ).resolves.toMatchObject({ + id: "https://example.org/ns/someCredentialInstance", + }); + expect(mockedFetch).toHaveBeenCalled(); + }); + it("returnLegacyJsonld: false", async () => { + await expect( + query( + "https://some.endpoint/query", + { query: [mockRequest] }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ), + ).resolves.toMatchObject({ + id: "https://example.org/ns/someCredentialInstance", + }); + expect(mockedFetch).toHaveBeenCalled(); + }); + }); + + it.each([ + ["null", [null]], + ["string", ["not a VC"]], + ["empty object", [{}]], + [ + "non empty object missing required VC properties", + [ + { + ...mockDefaultCredential(), + proof: undefined, + }, + ], + ], + ])( + "errors if the presentation contains invalid verifiable credentials [%s]", + async (_, elems) => { + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + // @ts-expect-error we are intentionall passing invalid VC types here to test for errors + new Response(JSON.stringify(mockDefaultPresentation(elems)), { + status: 200, + }), + ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + + await expect( + query( + "https://some.endpoint/query", + { query: [mockRequest] }, + { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + ), + ).rejects.toThrow(); + await expect( + query( + "https://some.endpoint/query", + { query: [mockRequest] }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ), + ).rejects.toThrow(); + }, + ); + it("defaults to an unauthenticated fetch if no fetch is provided", async () => { const mockedFetch = jest.requireMock( "@inrupt/universal-fetch", From 3aabb6c849f223babe34bc65a91feac2ca3be7fc Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 26 Nov 2023 12:30:37 +0000 Subject: [PATCH 51/79] chore: export VerifiableCredentialBase --- src/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index d4f53b4f..eec64bd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,12 @@ // export { default as issueVerifiableCredential } from "./issue/issue"; -export type { Iri, JsonLd, VerifiableCredential } from "./common/common"; +export type { + Iri, + JsonLd, + VerifiableCredential, + VerifiableCredentialBase, +} from "./common/common"; export { isVerifiableCredential, isVerifiablePresentation, From cfc5680dbdb3ab5912198a37a09dfddeb219602a Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 27 Nov 2023 07:55:00 +0000 Subject: [PATCH 52/79] chore: add normalization hook --- src/common/common.ts | 11 +++++++++-- src/index.ts | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index 1950d037..74bcbfc3 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -533,6 +533,7 @@ export async function internal_getVerifiableCredentialFromResponse( response: Response, options?: ParseOptions & { returnLegacyJsonld?: true; + normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise; export async function internal_getVerifiableCredentialFromResponse( @@ -547,6 +548,7 @@ export async function internal_getVerifiableCredentialFromResponse( response: Response, options?: ParseOptions & { returnLegacyJsonld?: boolean; + normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise { const returnLegacy = options?.returnLegacyJsonld !== false; @@ -577,7 +579,10 @@ export async function internal_getVerifiableCredentialFromResponse( `The value received from [${vcUrl}] is not a Verifiable Credential`, ); } - return verifiableCredentialToDataset(vc, { + if (options?.normalize) { + vc = options.normalize(vc as VerifiableCredentialBase); + } + return verifiableCredentialToDataset(vc as VerifiableCredentialBase, { allowContextFetching: options?.allowContextFetching, baseIRI: options?.baseIRI, contexts: options?.contexts, @@ -603,7 +608,7 @@ export async function internal_getVerifiableCredentialFromResponse( includeVcProperties: false, }); - if (!isRdfjsVerifiableCredential(parsedVc, namedNode(vcUrl))) { + if (!isRdfjsVerifiableCredential(parsedVc, namedNode(parsedVc.id))) { throw new Error( `The value received from [${vcUrl}] is not a Verifiable Credential`, ); @@ -628,6 +633,7 @@ export async function getVerifiableCredential( options?: ParseOptions & { fetch?: typeof fetch; returnLegacyJsonld?: true; + normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise; /** @@ -652,6 +658,7 @@ export async function getVerifiableCredential( options?: ParseOptions & { fetch?: typeof fetch; returnLegacyJsonld?: boolean; + normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise { const authFetch = options?.fetch ?? uniFetch; diff --git a/src/index.ts b/src/index.ts index eec64bd1..d613214a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,7 @@ export type { VerifiableCredential, VerifiableCredentialBase, } from "./common/common"; +export type { DatasetWithId } from "./common/getters"; export { isVerifiableCredential, isVerifiablePresentation, From 8b653653af16b7e4f3d7d4a2af300df012d02f9e Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 27 Nov 2023 08:07:17 +0000 Subject: [PATCH 53/79] chore: add normalization test --- src/common/common.test.ts | 45 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 0097db25..77334c6f 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -44,7 +44,7 @@ import { mockPartialCredential, mockPartialPresentation, } from "./common.mock"; -import { cred } from "./constants"; +import { cred, rdf } from "./constants"; const { namedNode, quad, blankNode } = DataFactory; @@ -1191,6 +1191,49 @@ describe("getVerifiableCredential", () => { ).resolves.toMatchObject(mockCredential); }); + it("can apply normalization of the response before parsing and returning it", async () => { + (uniFetch as jest.Mock).mockResolvedValueOnce( + new Response( + JSON.stringify({ + "@context": {}, + }), + { + headers: new Headers([["content-type", "application/ld+json"]]), + }, + ), + ); + + const res = await getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + allowContextFetching: true, + normalize: (data) => ({ + ...data, + type: [...data.type, "http://example.org/my/custom/added/type"], + }), + }, + ); + + expect(res).toMatchObject({ + ...mockCredential, + type: [ + ...mockDefaultCredential().type, + "http://example.org/my/custom/added/type", + ], + }); + + expect( + res.has( + quad( + namedNode("https://example.org/ns/someCredentialInstance"), + rdf.type, + namedNode("http://example.org/my/custom/added/type"), + ), + ), + ).toBe(true); + }); + it("resolves if allowContextFetching is enabled and the context can be fetched [returnLegacyJsonld: false]", async () => { (uniFetch as jest.Mock).mockResolvedValueOnce( new Response( From a8e2dc7144dc0fb2b0870d438226c46215a52a16 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:31:16 +0000 Subject: [PATCH 54/79] Update src/common/getters.ts Co-authored-by: Zwifi --- src/common/getters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/getters.ts b/src/common/getters.ts index 4bba0827..70cf491e 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -71,7 +71,7 @@ export function getCredentialSubject(vc: DatasetWithId): NamedNode { * @example * * ``` - * const date = getIssuer(vc); + * const issuer = getIssuer(vc); * ``` * * @param vc The Verifiable Credential From 1a01afef806267564618b6d74bdd1fbd14fefa6f Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:31:30 +0000 Subject: [PATCH 55/79] Update src/common/getters.ts Co-authored-by: Zwifi --- src/common/getters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/getters.ts b/src/common/getters.ts index 70cf491e..b010751a 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -50,7 +50,7 @@ export function getId(vc: DatasetWithId): string { * @example * * ``` - * const date = getCredentialSubject(vc); + * const subject = getCredentialSubject(vc); * ``` * * @param vc The Verifiable Credential From 13e9f015922f97ab3ad2d3dacf5b13ee542758e5 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:06:36 +0000 Subject: [PATCH 56/79] feat: add parameterizable normalization --- src/issue/issue.ts | 5 ++++- src/lookup/derive.ts | 3 +++ src/lookup/query.ts | 10 +++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/issue/issue.ts b/src/issue/issue.ts index 93b8a585..2d1e33db 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -25,7 +25,7 @@ import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; -import type { Iri, JsonLd, VerifiableCredential } from "../common/common"; +import type { Iri, JsonLd, VerifiableCredential, VerifiableCredentialBase } from "../common/common"; import { concatenateContexts, defaultContext, @@ -159,6 +159,7 @@ export async function issueVerifiableCredential( options?: { fetch?: typeof fallbackFetch; returnLegacyJsonld?: true; + normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise; /** @@ -185,6 +186,7 @@ export async function issueVerifiableCredential( options?: { fetch?: typeof fallbackFetch; returnLegacyJsonld?: boolean; + normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise; /** @@ -198,6 +200,7 @@ export async function issueVerifiableCredential( options?: { fetch?: typeof fallbackFetch; returnLegacyJsonld?: true; + normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise; /** diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index 92a9c03b..42ac7bd8 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -107,6 +107,7 @@ export async function getVerifiableCredentialAllFromShape( fetch: typeof fallbackFetch; includeExpiredVc: boolean; returnLegacyJsonld?: true; + normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }>, ): Promise; /** @@ -144,6 +145,7 @@ export async function getVerifiableCredentialAllFromShape( fetch: typeof fallbackFetch; includeExpiredVc: boolean; returnLegacyJsonld?: boolean; + normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }>, ): Promise { const fetchFn = options?.fetch ?? fallbackFetch; @@ -163,6 +165,7 @@ export async function getVerifiableCredentialAllFromShape( const vp = await query(holderEndpoint, vpRequest, { fetch: fetchFn, returnLegacyJsonld: options?.returnLegacyJsonld, + normalize: options?.normalize }); return vp.verifiableCredential ?? []; } diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 7fa15209..d2e8c7f7 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -86,6 +86,7 @@ export async function query( Partial<{ fetch: typeof fallbackFetch; returnLegacyJsonld?: true; + normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }>, ): Promise; /** @@ -127,6 +128,7 @@ export async function query( Partial<{ fetch: typeof fallbackFetch; returnLegacyJsonld: false; + normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }>, ): Promise<{ verifiableCredential?: DatasetWithId[] }>; export async function query( @@ -136,6 +138,7 @@ export async function query( Partial<{ fetch: typeof fallbackFetch; returnLegacyJsonld?: boolean; + normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }>, ): Promise< ParsedVerifiablePresentation | { verifiableCredential?: DatasetWithId[] } @@ -147,6 +150,7 @@ export async function query( Partial<{ fetch: typeof fallbackFetch; returnLegacyJsonld?: boolean; + normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }>, ): Promise< ParsedVerifiablePresentation | { verifiableCredential?: DatasetWithId[] } @@ -259,11 +263,15 @@ export async function query( ...(await Promise.all( data.verifiableCredential .slice(i, i + 100) - .map(async (vc: unknown) => { + .map(async (vc: VerifiableCredentialBase) => { if (typeof vc !== "object" || vc === null) { throw new Error(`Verifiable Credentail is an invalid object`); } + if (options?.normalize) { + vc = options.normalize(vc); + } + const res = await verifiableCredentialToDataset(vc, { ...options, includeVcProperties: options?.returnLegacyJsonld !== false, From a372474fe58883a88335de83a37e1edabe2542b4 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:18:05 +0000 Subject: [PATCH 57/79] chore: update JSDoc --- src/lookup/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lookup/query.ts b/src/lookup/query.ts index d2e8c7f7..b2b920e9 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -77,7 +77,7 @@ interface ParsedVerifiablePresentation extends VerifiablePresentation { } /** - * @deprecated Use RDFJS API + * @deprecated Use RDFJS API instead of relying on the JSON structure by setting `returnLegacyJsonld` to false */ export async function query( queryEndpoint: Iri, From 9bedd1747924b157038456ee64634066ac74b255 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:30:16 +0000 Subject: [PATCH 58/79] chore: fixup type overloads --- src/common/common.ts | 40 +++++++++++++++++++++++++++++ src/issue/issue.ts | 45 +++++++++++++++++++++++++++++++++ src/lookup/derive.ts | 60 +++++++++++++++++++++++++++++++++----------- src/lookup/query.ts | 34 ++++++++++++++----------- 4 files changed, 150 insertions(+), 29 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index 74bcbfc3..0b82553b 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -528,6 +528,17 @@ export async function verifiableCredentialToDataset( } // eslint-disable-next-line camelcase +export async function internal_getVerifiableCredentialFromResponse( + vcUrl: UrlString | undefined, + response: Response, + options: ParseOptions & { + returnLegacyJsonld: false; + }, +): Promise; +/** + * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future + * versions of this library. + */ export async function internal_getVerifiableCredentialFromResponse( vcUrl: UrlString | undefined, response: Response, @@ -536,6 +547,10 @@ export async function internal_getVerifiableCredentialFromResponse( normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise; +/** + * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future + * versions of this library. + */ export async function internal_getVerifiableCredentialFromResponse( vcUrl: UrlString | undefined, response: Response, @@ -564,6 +579,8 @@ export async function internal_getVerifiableCredentialFromResponse( vcUrl = vc.id; } + // If you're wondering why this is not inside the if (returnLegacy) condition outside this try/catch statement + // see https://github.com/inrupt/solid-client-vc-js/pull/849#discussion_r1405853022 if (returnLegacy) { vc = normalizeVc(vc); } @@ -591,6 +608,8 @@ export async function internal_getVerifiableCredentialFromResponse( } if ( + // This is needed to make typescript happy; + // the compiler infers the type to be `null | undefined | {}` when it reaches this line. typeof vc !== "object" || vc === null || !("id" in vc) || @@ -616,6 +635,24 @@ export async function internal_getVerifiableCredentialFromResponse( return parsedVc; } +/** + * Dereference a VC URL, and verify that the resulting content is valid. + * + * @param vcUrl The URL of the VC. + * @param options Options to customize the function behavior. + * - options.fetch: Specify a WHATWG-compatible authenticated fetch. + * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response + * @returns The dereferenced VC if valid. Throws otherwise. + * @since 0.4.0 + */ +export async function getVerifiableCredential( + vcUrl: UrlString, + options: ParseOptions & { + fetch?: typeof fetch; + returnLegacyJsonld: false; + normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; + }, +): Promise; /** * Dereference a VC URL, and verify that the resulting content is valid. * @@ -645,12 +682,15 @@ export async function getVerifiableCredential( * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response * @returns The dereferenced VC if valid. Throws otherwise. * @since 0.4.0 + * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future + * versions of this library. */ export async function getVerifiableCredential( vcUrl: UrlString, options?: ParseOptions & { fetch?: typeof fetch; returnLegacyJsonld?: boolean; + normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise; export async function getVerifiableCredential( diff --git a/src/issue/issue.ts b/src/issue/issue.ts index 2d1e33db..484bb4a6 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -45,6 +45,19 @@ type OptionsType = { // from the deprecation logic. It can be merged back in issueVerifiableCredential // when the deprecated signature is removed altogether. // eslint-disable-next-line camelcase +async function internal_issueVerifiableCredential( + issuerEndpoint: Iri, + subjectClaims: JsonLd, + credentialClaims: JsonLd, + options: { + fetch?: typeof fallbackFetch; + returnLegacyJsonld: false; + } & ParseOptions, +): Promise; +/** + * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future + * versions of this library. + */ async function internal_issueVerifiableCredential( issuerEndpoint: Iri, subjectClaims: JsonLd, @@ -54,6 +67,10 @@ async function internal_issueVerifiableCredential( returnLegacyJsonld?: true; } & ParseOptions, ): Promise; +/** + * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future + * versions of this library. + */ async function internal_issueVerifiableCredential( issuerEndpoint: Iri, subjectClaims: JsonLd, @@ -133,6 +150,32 @@ async function internal_issueVerifiableCredential( ); } +/** + * Request that a given Verifiable Credential (VC) Issuer issues a VC containing + * the provided claims. The VC Issuer is expected to implement the [W3C VC Issuer HTTP API](https://w3c-ccg.github.io/vc-api/issuer.html). + * + * @param issuerEndpoint The `/issue` endpoint of the VC Issuer. + * @param subjectId The identifier of the VC claims' subject. + * @param subjectClaims Claims about the subject that will be attested by the VC. + * @param credentialClaims Claims about the credential itself, rather than its subject, e.g. credential type or expiration. + * @param options + * - options.fetch: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). + * This can be typically used for authentication. Note that if it is omitted, and + * `@inrupt/solid-client-authn-browser` is in your dependencies, the default session + * is picked up. + * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response + * @returns the VC returned by the Issuer if the request is successful. Otherwise, an error is thrown. + * @since 0.1.0 + */ +export async function issueVerifiableCredential( + issuerEndpoint: Iri, + subjectClaims: JsonLd, + credentialClaims: JsonLd, + options: { + fetch?: typeof fallbackFetch; + returnLegacyJsonld: false; + }, +): Promise; /** * Request that a given Verifiable Credential (VC) Issuer issues a VC containing * the provided claims. The VC Issuer is expected to implement the [W3C VC Issuer HTTP API](https://w3c-ccg.github.io/vc-api/issuer.html). @@ -178,6 +221,8 @@ export async function issueVerifiableCredential( * - options.returnLegacyJsonld: Include the normalized JSON-LD in the response * @returns the VC returned by the Issuer if the request is successful. Otherwise, an error is thrown. * @since 0.1.0 + * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future + * versions of this library. */ export async function issueVerifiableCredential( issuerEndpoint: Iri, diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index 42ac7bd8..aaf77cb0 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -98,17 +98,46 @@ function buildQueryByExample( * @returns A list of VCs matching the given VC shape. The list may be empty if * the holder does not hold any matching VC. * @since 0.1.0 - * @deprecated set options.includeExpiredVc to false and use RDFJS API instead */ export async function getVerifiableCredentialAllFromShape( holderEndpoint: Iri, vcShape: Partial, - options?: Partial<{ - fetch: typeof fallbackFetch; - includeExpiredVc: boolean; + options: { + fetch?: typeof fallbackFetch; + includeExpiredVc?: boolean; + returnLegacyJsonld: false; + }, +): Promise; +/** + * Look up VCs from a given holder according to a subset of their claims, such as + * the VC type, or any property associated to the subject in the VC. The holder + * is expected to implement the [W3C VC Holder HTTP API](https://w3c-ccg.github.io/vc-api/holder.html). + * + * @param holderEndpoint The `/derive` endpoint of the holder. + * @param vcShape The subset of claims you expect the matching VCs to contain. + * @param options Optional parameter: + * - `options.fetch`: An alternative `fetch` function to make the HTTP request, compatible with the browser-native [fetch API](https://developer.mozilla.org/docs/Web/API/WindowOrWorkerGlobalScope/fetch#parameters). + * This can be typically used for authentication. Note that if it is omitted, and + * `@inrupt/solid-client-authn-browser` is in your dependencies, the default session + * is picked up. + * - `options.includeExpiredVc`: include expired VC matching the shape in the + * result set. + * - `options.returnLegacyJsonld`: : Include the normalized JSON-LD in the response + * @returns A list of VCs matching the given VC shape. The list may be empty if + * the holder does not hold any matching VC. + * @since 0.1.0 + * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future + * versions of this library. + */ +export async function getVerifiableCredentialAllFromShape( + holderEndpoint: Iri, + vcShape: Partial, + options?: { + fetch?: typeof fallbackFetch; + includeExpiredVc?: boolean; returnLegacyJsonld?: true; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; - }>, + }, ): Promise; /** * Look up VCs from a given holder according to a subset of their claims, such as @@ -128,25 +157,28 @@ export async function getVerifiableCredentialAllFromShape( * @returns A list of VCs matching the given VC shape. The list may be empty if * the holder does not hold any matching VC. * @since 0.1.0 + * @deprecated Deprecated in favour of setting returnLegacyJsonld: false. This will be the default value in future + * versions of this library. */ export async function getVerifiableCredentialAllFromShape( holderEndpoint: Iri, vcShape: Partial, - options?: Partial<{ - fetch: typeof fallbackFetch; - includeExpiredVc: boolean; - returnLegacyJsonld: false; - }>, + options?: { + fetch?: typeof fallbackFetch; + includeExpiredVc?: boolean; + returnLegacyJsonld?: boolean; + normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; + }, ): Promise; export async function getVerifiableCredentialAllFromShape( holderEndpoint: Iri, vcShape: Partial, - options?: Partial<{ - fetch: typeof fallbackFetch; - includeExpiredVc: boolean; + options?: { + fetch?: typeof fallbackFetch; + includeExpiredVc?: boolean; returnLegacyJsonld?: boolean; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; - }>, + }, ): Promise { const fetchFn = options?.fetch ?? fallbackFetch; // The request payload depends on the target endpoint. diff --git a/src/lookup/query.ts b/src/lookup/query.ts index b2b920e9..2be14878 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -76,19 +76,7 @@ interface ParsedVerifiablePresentation extends VerifiablePresentation { verifiableCredential?: VerifiableCredential[]; } -/** - * @deprecated Use RDFJS API instead of relying on the JSON structure by setting `returnLegacyJsonld` to false - */ -export async function query( - queryEndpoint: Iri, - vpRequest: VerifiablePresentationRequest, - options?: ParseOptions & - Partial<{ - fetch: typeof fallbackFetch; - returnLegacyJsonld?: true; - normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; - }>, -): Promise; + /** * Send a Verifiable Presentation Request to a query endpoint in order to retrieve * all Verifiable Credentials matching the query, wrapped in a single Presentation. @@ -121,16 +109,32 @@ export async function query( * @param options Options object, including an authenticated `fetch`. * @returns The resulting Verifiable Presentation wrapping all the Credentials matching the query. */ +export async function query( + queryEndpoint: Iri, + vpRequest: VerifiablePresentationRequest, + options: ParseOptions & + { + fetch: typeof fallbackFetch; + returnLegacyJsonld: false; + normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; + }, +): Promise<{ verifiableCredential?: DatasetWithId[] }>; +/** + * @deprecated Use RDFJS API instead of relying on the JSON structure by setting `returnLegacyJsonld` to false + */ export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, options?: ParseOptions & Partial<{ fetch: typeof fallbackFetch; - returnLegacyJsonld: false; + returnLegacyJsonld?: true; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }>, -): Promise<{ verifiableCredential?: DatasetWithId[] }>; +): Promise; +/** + * @deprecated Use RDFJS API instead of relying on the JSON structure by setting `returnLegacyJsonld` to false + */ export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, From 4b64ce3192520b5602564e82cf9a2619b8e799b4 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:36:27 +0000 Subject: [PATCH 59/79] chore: resolve test coverage for normalization --- src/issue/issue.ts | 7 ++++++- src/lookup/derive.ts | 2 +- src/lookup/query.test.ts | 36 ++++++++++++++++++++++++++++++++++++ src/lookup/query.ts | 15 +++++++-------- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/issue/issue.ts b/src/issue/issue.ts index 484bb4a6..69d900c3 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -25,7 +25,12 @@ import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; -import type { Iri, JsonLd, VerifiableCredential, VerifiableCredentialBase } from "../common/common"; +import type { + Iri, + JsonLd, + VerifiableCredential, + VerifiableCredentialBase, +} from "../common/common"; import { concatenateContexts, defaultContext, diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index aaf77cb0..acd3e034 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -197,7 +197,7 @@ export async function getVerifiableCredentialAllFromShape( const vp = await query(holderEndpoint, vpRequest, { fetch: fetchFn, returnLegacyJsonld: options?.returnLegacyJsonld, - normalize: options?.normalize + normalize: options?.normalize, }); return vp.verifiableCredential ?? []; } diff --git a/src/lookup/query.test.ts b/src/lookup/query.test.ts index 63273d55..fc77591b 100644 --- a/src/lookup/query.test.ts +++ b/src/lookup/query.test.ts @@ -354,5 +354,41 @@ describe("query", () => { ], ).toBeUndefined(); }); + + it("applies additional normalisation to the vc's according to the normalize function", async () => { + const mockedVc = mockDefaultCredential(); + // Force unexpected VC shapes to check normalization. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + mockedVc.proof["https://w3id.org/security#proofValue"] = + mockedVc.proof.proofValue; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete mockedVc.proof.proofValue; + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mockDefaultPresentation([mockedVc])), { + status: 200, + }), + ); + const resultVp = await query( + "https://example.org/query", + { query: [mockRequest] }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + normalize(vc) { + return { + ...vc, + type: [...vc.type, "http://example.org/my/extra/type"], + }; + }, + }, + ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(resultVp.verifiableCredential![0].type).toContain( + "http://example.org/my/extra/type", + ); + }); }); }); diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 2be14878..695d8ad3 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -76,7 +76,6 @@ interface ParsedVerifiablePresentation extends VerifiablePresentation { verifiableCredential?: VerifiableCredential[]; } - /** * Send a Verifiable Presentation Request to a query endpoint in order to retrieve * all Verifiable Credentials matching the query, wrapped in a single Presentation. @@ -112,12 +111,11 @@ interface ParsedVerifiablePresentation extends VerifiablePresentation { export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, - options: ParseOptions & - { - fetch: typeof fallbackFetch; - returnLegacyJsonld: false; - normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; - }, + options: ParseOptions & { + fetch: typeof fallbackFetch; + returnLegacyJsonld: false; + normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; + }, ): Promise<{ verifiableCredential?: DatasetWithId[] }>; /** * @deprecated Use RDFJS API instead of relying on the JSON structure by setting `returnLegacyJsonld` to false @@ -267,7 +265,8 @@ export async function query( ...(await Promise.all( data.verifiableCredential .slice(i, i + 100) - .map(async (vc: VerifiableCredentialBase) => { + .map(async (_vc: VerifiableCredentialBase) => { + let vc = _vc; if (typeof vc !== "object" || vc === null) { throw new Error(`Verifiable Credentail is an invalid object`); } From c08515190497dcfc0542e2d8c3b88a100264ad07 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:59:18 +0000 Subject: [PATCH 60/79] chore: fix circular deps --- src/common/common.test.ts | 39 ++++++------ src/common/common.ts | 5 +- src/common/getters.test.ts | 3 +- src/common/getters.ts | 68 ++------------------- src/common/isRdfjsVerifiableCredential.ts | 55 +++++++++++++++++ src/common/isRdfjsVerifiablePresentation.ts | 57 +++++++++++++++++ src/index.test.ts | 4 +- src/index.ts | 6 +- src/issue/issue.ts | 2 +- src/lookup/derive.ts | 2 +- src/lookup/query.ts | 4 +- 11 files changed, 149 insertions(+), 96 deletions(-) create mode 100644 src/common/isRdfjsVerifiableCredential.ts create mode 100644 src/common/isRdfjsVerifiablePresentation.ts diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 77334c6f..215d6dc5 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -20,10 +20,10 @@ // import type * as UniversalFetch from "@inrupt/universal-fetch"; import { Response, fetch as uniFetch } from "@inrupt/universal-fetch"; -import { describe, expect, it, jest, beforeEach } from "@jest/globals"; +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import type { IJsonLdContext } from "jsonld-context-parser"; import { DataFactory, Store } from "n3"; import { isomorphic } from "rdf-isomorphic"; -import type { IJsonLdContext } from "jsonld-context-parser"; import { jsonLdStringToStore } from "../parser/jsonld"; import type { VerifiableCredential } from "./common"; import { @@ -34,7 +34,6 @@ import { normalizeVc, verifiableCredentialToDataset, } from "./common"; -import * as getters from "./getters"; import { defaultCredentialClaims, defaultVerifiableClaims, @@ -45,6 +44,8 @@ import { mockPartialPresentation, } from "./common.mock"; import { cred, rdf } from "./constants"; +import isRdfjsVerifiableCredential from "./isRdfjsVerifiableCredential"; +import isRdfjsVerifiablePresentation from "./isRdfjsVerifiablePresentation"; const { namedNode, quad, blankNode } = DataFactory; @@ -71,7 +72,7 @@ describe("isVerifiableCredential", () => { it("returns true if all the expected fields are present in the credential", async () => { expect(isVerifiableCredential(mockDefaultCredential())).toBe(true); expect( - getters.isVerifiableCredential( + isRdfjsVerifiableCredential( await verifiableCredentialToDataset(mockDefaultCredential()), namedNode(mockDefaultCredential().id), ), @@ -94,7 +95,7 @@ describe("isVerifiableCredential", () => { if (entry !== "id") { // eslint-disable-next-line jest/no-conditional-expect expect( - getters.isVerifiableCredential( + isRdfjsVerifiableCredential( await verifiableCredentialToDataset( mockPartialCredential({ ...defaultCredentialClaims, @@ -138,7 +139,7 @@ describe("isVerifiableCredential", () => { } ).credentialSubject; expect( - getters.isVerifiableCredential( + isRdfjsVerifiableCredential( await verifiableCredentialToDataset(mockedCredential), namedNode(mockDefaultCredential().id), ), @@ -158,7 +159,7 @@ describe("isVerifiableCredential", () => { it("has an unexpected date format for the issuance", async () => { expect( - getters.isVerifiableCredential( + isRdfjsVerifiableCredential( await verifiableCredentialToDataset( mockPartialCredential({ ...defaultCredentialClaims, @@ -181,7 +182,7 @@ describe("isVerifiableCredential", () => { it("has an unexpected date format for the proof creation", async () => { expect( - getters.isVerifiableCredential( + isRdfjsVerifiableCredential( await verifiableCredentialToDataset( mockPartialCredential({ ...defaultCredentialClaims, @@ -209,7 +210,7 @@ describe("isVerifiablePresentation", () => { it("has all the expected fields are present in the credential", async () => { expect(isVerifiablePresentation(mockDefaultPresentation())).toBe(true); expect( - getters.isVerifiablePresentation( + isRdfjsVerifiablePresentation( await verifiableCredentialToDataset( mockDefaultPresentation() as { id: string }, ), @@ -221,7 +222,7 @@ describe("isVerifiablePresentation", () => { it("has no associated credentials", async () => { expect(isVerifiablePresentation(mockDefaultPresentation([]))).toBe(true); expect( - getters.isVerifiablePresentation( + isRdfjsVerifiablePresentation( await verifiableCredentialToDataset( mockDefaultPresentation([]) as { id: string }, ), @@ -235,7 +236,7 @@ describe("isVerifiablePresentation", () => { mockedPresentation.holder = "https://some.holder"; expect(isVerifiablePresentation(mockedPresentation)).toBe(true); expect( - getters.isVerifiablePresentation( + isRdfjsVerifiablePresentation( await verifiableCredentialToDataset( mockedPresentation as { id: string }, ), @@ -299,7 +300,7 @@ describe("isVerifiablePresentation", () => { }; expect(isVerifiablePresentation(vp)).toBe(true); expect( - getters.isVerifiablePresentation( + isRdfjsVerifiablePresentation( await verifiableCredentialToDataset(vp), namedNode(vp.id), ), @@ -323,7 +324,7 @@ describe("isVerifiablePresentation", () => { ), ).toBe(false); expect( - getters.isVerifiablePresentation( + isRdfjsVerifiablePresentation( await verifiableCredentialToDataset(vp), // @ts-expect-error id is of type unknown namedNode(vp.id), @@ -345,7 +346,7 @@ describe("isVerifiablePresentation", () => { ).toBe(0); expect(isVerifiablePresentation(mockedPresentation)).toBe(false); expect( - getters.isVerifiablePresentation( + isRdfjsVerifiablePresentation( mockedPresentationAsDataset, namedNode(mockedPresentation.id!), ), @@ -353,7 +354,7 @@ describe("isVerifiablePresentation", () => { // Should return false when we artifically add a blank node to the dataset expect( - getters.isVerifiablePresentation( + isRdfjsVerifiablePresentation( new Store([ ...mockedPresentationAsDataset, quad( @@ -367,7 +368,7 @@ describe("isVerifiablePresentation", () => { ).toBe(false); // Should return false when we artifically add a named node with invalid url to the dataset expect( - getters.isVerifiablePresentation( + isRdfjsVerifiablePresentation( new Store([ ...mockedPresentationAsDataset, quad( @@ -391,7 +392,7 @@ describe("isVerifiablePresentation", () => { ); expect(presentationAsDataset.match(null, cred.holder, null).size).toBe(0); expect( - getters.isVerifiablePresentation( + isRdfjsVerifiablePresentation( presentationAsDataset, namedNode(mockedPresentation.id!), ), @@ -399,7 +400,7 @@ describe("isVerifiablePresentation", () => { // Should return false when we artifically add a blank node to the dataset expect( - getters.isVerifiablePresentation( + isRdfjsVerifiablePresentation( new Store([ ...presentationAsDataset, quad(namedNode(mockedPresentation.id!), cred.holder, blankNode()), @@ -409,7 +410,7 @@ describe("isVerifiablePresentation", () => { ).toBe(false); // Should return false when we artifically add a named node with invalid url to the dataset expect( - getters.isVerifiablePresentation( + isRdfjsVerifiablePresentation( new Store([ ...presentationAsDataset, quad( diff --git a/src/common/common.ts b/src/common/common.ts index 0b82553b..fc3f2e74 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -35,11 +35,12 @@ import type { DatasetCore, Quad } from "@rdfjs/types"; import { DataFactory } from "n3"; import type { ParseOptions } from "../parser/jsonld"; import { jsonLdToStore } from "../parser/jsonld"; -import { isVerifiableCredential as isRdfjsVerifiableCredential } from "./getters"; -import type { DatasetWithId } from "./getters"; +import isRdfjsVerifiableCredential from "./isRdfjsVerifiableCredential"; const { namedNode } = DataFactory; +export type DatasetWithId = DatasetCore & { id: string }; + export type Iri = string; /** * A JSON-LD document is a JSON document including an @context entry. The other diff --git a/src/common/getters.test.ts b/src/common/getters.test.ts index 26513195..5ee9b263 100644 --- a/src/common/getters.test.ts +++ b/src/common/getters.test.ts @@ -21,11 +21,10 @@ import { beforeAll, describe, expect, it } from "@jest/globals"; import { Store, DataFactory } from "n3"; -import type { VerifiableCredential } from "./common"; +import type { VerifiableCredential, DatasetWithId } from "./common"; import { verifiableCredentialToDataset } from "./common"; import { cred, xsd } from "./constants"; import { mockDefaultCredential } from "./common.mock"; -import type { DatasetWithId } from "./getters"; import { getCredentialSubject, getExpirationDate, diff --git a/src/common/getters.ts b/src/common/getters.ts index b010751a..a3398212 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -20,13 +20,11 @@ // import type { BlankNode, DatasetCore, Literal, NamedNode } from "@rdfjs/types"; import { DataFactory } from "n3"; -import { isUrl } from "./common"; +import { type DatasetWithId } from "./common"; import { cred, dc, rdf, sec, xsd } from "./constants"; import { getSingleObject, lenientSingle } from "./rdfjs"; -const { namedNode, defaultGraph, quad } = DataFactory; - -export type DatasetWithId = DatasetCore & { id: string }; +const { namedNode, defaultGraph } = DataFactory; /** * Get the ID (URL) of a Verifiable Credential. @@ -157,7 +155,7 @@ export function getExpirationDate(vc: DatasetWithId): Date | undefined { /** * @internal */ -function isDate(literal?: Literal): boolean { +export function isDate(literal?: Literal): boolean { return ( !!literal && literal.datatype.equals(xsd.dateTime) && @@ -165,7 +163,7 @@ function isDate(literal?: Literal): boolean { ); } -function isValidProof( +export function isValidProof( dataset: DatasetCore, proof: NamedNode | BlankNode, ): boolean { @@ -191,61 +189,3 @@ function isValidProof( ]) !== undefined ); } - -export function isVerifiableCredential( - dataset: DatasetCore, - id: NamedNode, -): boolean { - const proof = lenientSingle( - dataset.match(id, sec.proof, null, defaultGraph()), - ); - return ( - !!proof && - isValidProof(dataset, proof) && - !!lenientSingle( - dataset.match(id, cred.issuer, null, defaultGraph()), - ["NamedNode"], - ) && - isDate( - lenientSingle( - dataset.match(id, cred.issuanceDate, null, defaultGraph()), - ["Literal"], - ), - ) && - !!lenientSingle( - dataset.match(id, cred.credentialSubject, null, defaultGraph()), - ["NamedNode"], - ) && - dataset.has(quad(id, rdf.type, cred.VerifiableCredential, defaultGraph())) - ); -} - -export function isVerifiablePresentation( - dataset: DatasetCore, - id: NamedNode | BlankNode, -): boolean { - for (const { object } of dataset.match( - id, - cred.verifiableCredential, - null, - defaultGraph(), - )) { - if ( - object.termType !== "NamedNode" || - !isVerifiableCredential(dataset, object) - ) { - return false; - } - } - - const holder = [...dataset.match(id, cred.holder, null, defaultGraph())]; - return ( - (holder.length === 0 || - (holder.length === 1 && - holder[0].object.termType === "NamedNode" && - isUrl(holder[0].object.value))) && - // dataset.has(quad(id, rdf.type, cred.VerifiablePresentation, defaultGraph())) - // FIXME: Replace with the above condition - dataset.match(id, rdf.type, null, defaultGraph()).size >= 1 - ); -} diff --git a/src/common/isRdfjsVerifiableCredential.ts b/src/common/isRdfjsVerifiableCredential.ts new file mode 100644 index 00000000..e2fd1313 --- /dev/null +++ b/src/common/isRdfjsVerifiableCredential.ts @@ -0,0 +1,55 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +import type { BlankNode, DatasetCore, Literal, NamedNode } from "@rdfjs/types"; +import { DataFactory } from "n3"; +import { cred, rdf, sec } from "./constants"; +import { lenientSingle } from "./rdfjs"; +import { isValidProof, isDate } from "./getters"; + +const { defaultGraph, quad } = DataFactory; + +export default function isRdfjsVerifiableCredential( + dataset: DatasetCore, + id: NamedNode, +): boolean { + const proof = lenientSingle( + dataset.match(id, sec.proof, null, defaultGraph()), + ); + return ( + !!proof && + isValidProof(dataset, proof) && + !!lenientSingle( + dataset.match(id, cred.issuer, null, defaultGraph()), + ["NamedNode"], + ) && + isDate( + lenientSingle( + dataset.match(id, cred.issuanceDate, null, defaultGraph()), + ["Literal"], + ), + ) && + !!lenientSingle( + dataset.match(id, cred.credentialSubject, null, defaultGraph()), + ["NamedNode"], + ) && + dataset.has(quad(id, rdf.type, cred.VerifiableCredential, defaultGraph())) + ); +} diff --git a/src/common/isRdfjsVerifiablePresentation.ts b/src/common/isRdfjsVerifiablePresentation.ts new file mode 100644 index 00000000..9fda245c --- /dev/null +++ b/src/common/isRdfjsVerifiablePresentation.ts @@ -0,0 +1,57 @@ +// +// Copyright Inrupt Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +import type { BlankNode, DatasetCore, NamedNode } from "@rdfjs/types"; +import { DataFactory } from "n3"; +import { isUrl } from "./common"; +import { cred, rdf } from "./constants"; +import isRdfjsVerifiableCredential from "./isRdfjsVerifiableCredential"; + +const { defaultGraph } = DataFactory; + +export default function isRdfjsVerifiablePresentation( + dataset: DatasetCore, + id: NamedNode | BlankNode, +): boolean { + for (const { object } of dataset.match( + id, + cred.verifiableCredential, + null, + defaultGraph(), + )) { + if ( + object.termType !== "NamedNode" || + !isRdfjsVerifiableCredential(dataset, object) + ) { + return false; + } + } + + const holder = [...dataset.match(id, cred.holder, null, defaultGraph())]; + return ( + (holder.length === 0 || + (holder.length === 1 && + holder[0].object.termType === "NamedNode" && + isUrl(holder[0].object.value))) && + // dataset.has(quad(id, rdf.type, cred.VerifiablePresentation, defaultGraph())) + // FIXME: Replace with the above condition + dataset.match(id, rdf.type, null, defaultGraph()).size >= 1 + ); +} diff --git a/src/index.test.ts b/src/index.test.ts index f5031f1e..f105e97d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -34,13 +34,13 @@ import { getExpirationDate, getIssuanceDate, getIssuer, - isVerifiableCredential as isRdfjsVerifiableCredential, - isVerifiablePresentation as isRdfjsVerifiablePresentation, } from "./common/getters"; import getVerifiableCredentialAllFromShape from "./lookup/derive"; import revokeVerifiableCredential from "./revoke/revoke"; import { isValidVc, isValidVerifiablePresentation } from "./verify/verify"; import { query } from "./lookup/query"; +import isRdfjsVerifiableCredential from "./common/isRdfjsVerifiableCredential"; +import isRdfjsVerifiablePresentation from "./common/isRdfjsVerifiablePresentation"; describe("exports", () => { it("includes all of the expected functions", () => { diff --git a/src/index.ts b/src/index.ts index d613214a..55020408 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,7 @@ export type { VerifiableCredential, VerifiableCredentialBase, } from "./common/common"; -export type { DatasetWithId } from "./common/getters"; +export type { DatasetWithId } from "./common/common"; export { isVerifiableCredential, isVerifiablePresentation, @@ -51,6 +51,6 @@ export { getIssuer, getCredentialSubject, getExpirationDate, - isVerifiableCredential as isRdfjsVerifiableCredential, - isVerifiablePresentation as isRdfjsVerifiablePresentation, } from "./common/getters"; +export { default as isRdfjsVerifiableCredential } from "./common/isRdfjsVerifiableCredential"; +export { default as isRdfjsVerifiablePresentation } from "./common/isRdfjsVerifiablePresentation"; diff --git a/src/issue/issue.ts b/src/issue/issue.ts index 69d900c3..f2551bda 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -30,6 +30,7 @@ import type { JsonLd, VerifiableCredential, VerifiableCredentialBase, + DatasetWithId, } from "../common/common"; import { concatenateContexts, @@ -39,7 +40,6 @@ import { internal_getVerifiableCredentialFromResponse, } from "../common/common"; import type { ParseOptions } from "../parser/jsonld"; -import type { DatasetWithId } from "../common/getters"; type OptionsType = { fetch?: typeof fallbackFetch; diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index acd3e034..70853b5f 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -24,11 +24,11 @@ import type { Iri, VerifiableCredential, VerifiableCredentialBase, + DatasetWithId, } from "../common/common"; import { concatenateContexts, defaultContext } from "../common/common"; import type { VerifiablePresentationRequest } from "./query"; import { query } from "./query"; -import type { DatasetWithId } from "../common/getters"; const INCLUDE_EXPIRED_VC_OPTION = "ExpiredVerifiableCredential" as const; diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 695d8ad3..8c979bc7 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -21,19 +21,19 @@ import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; import { DataFactory } from "n3"; -import { isRdfjsVerifiableCredential } from ".."; +import isRdfjsVerifiableCredential from "../common/isRdfjsVerifiableCredential"; import type { Iri, VerifiableCredential, VerifiableCredentialBase, VerifiablePresentation, + DatasetWithId, } from "../common/common"; import { isVerifiablePresentation, normalizeVp, verifiableCredentialToDataset, } from "../common/common"; -import type { DatasetWithId } from "../common/getters"; import { type ParseOptions } from "../parser/jsonld"; const { namedNode } = DataFactory; From 6b77d2f6b33918b74684af1751a1f55bf764a6b8 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 30 Nov 2023 16:22:35 +0000 Subject: [PATCH 61/79] chore: add skipValidation option --- src/common/common.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index fc3f2e74..0574d377 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -534,6 +534,7 @@ export async function internal_getVerifiableCredentialFromResponse( response: Response, options: ParseOptions & { returnLegacyJsonld: false; + skipValidation?: boolean; }, ): Promise; /** @@ -545,6 +546,7 @@ export async function internal_getVerifiableCredentialFromResponse( response: Response, options?: ParseOptions & { returnLegacyJsonld?: true; + skipValidation?: boolean; normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise; @@ -557,6 +559,8 @@ export async function internal_getVerifiableCredentialFromResponse( response: Response, options?: ParseOptions & { returnLegacyJsonld?: boolean; + skipValidation?: boolean; + noVerify?: boolean; }, ): Promise; export async function internal_getVerifiableCredentialFromResponse( @@ -564,6 +568,7 @@ export async function internal_getVerifiableCredentialFromResponse( response: Response, options?: ParseOptions & { returnLegacyJsonld?: boolean; + skipValidation?: boolean; normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise { @@ -592,7 +597,7 @@ export async function internal_getVerifiableCredentialFromResponse( } if (returnLegacy) { - if (!isVerifiableCredential(vc)) { + if (!options?.skipValidation && !isVerifiableCredential(vc)) { throw new Error( `The value received from [${vcUrl}] is not a Verifiable Credential`, ); @@ -628,7 +633,7 @@ export async function internal_getVerifiableCredentialFromResponse( includeVcProperties: false, }); - if (!isRdfjsVerifiableCredential(parsedVc, namedNode(parsedVc.id))) { + if (!options.skipValidation && !isRdfjsVerifiableCredential(parsedVc, namedNode(parsedVc.id))) { throw new Error( `The value received from [${vcUrl}] is not a Verifiable Credential`, ); @@ -650,6 +655,7 @@ export async function getVerifiableCredential( vcUrl: UrlString, options: ParseOptions & { fetch?: typeof fetch; + skipValidation?: boolean; returnLegacyJsonld: false; normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, @@ -670,6 +676,7 @@ export async function getVerifiableCredential( vcUrl: UrlString, options?: ParseOptions & { fetch?: typeof fetch; + skipValidation?: boolean; returnLegacyJsonld?: true; normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, @@ -690,6 +697,7 @@ export async function getVerifiableCredential( vcUrl: UrlString, options?: ParseOptions & { fetch?: typeof fetch; + skipValidation?: boolean; returnLegacyJsonld?: boolean; normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, @@ -698,6 +706,7 @@ export async function getVerifiableCredential( vcUrl: UrlString, options?: ParseOptions & { fetch?: typeof fetch; + skipValidation?: boolean; returnLegacyJsonld?: boolean; normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, From 5f923d26dd60744ebbaf2c376fd6fb9b63aa2af8 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 30 Nov 2023 16:23:18 +0000 Subject: [PATCH 62/79] chore: fix lint errors --- src/common/common.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/common/common.ts b/src/common/common.ts index 0574d377..94092704 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -633,7 +633,10 @@ export async function internal_getVerifiableCredentialFromResponse( includeVcProperties: false, }); - if (!options.skipValidation && !isRdfjsVerifiableCredential(parsedVc, namedNode(parsedVc.id))) { + if ( + !options.skipValidation && + !isRdfjsVerifiableCredential(parsedVc, namedNode(parsedVc.id)) + ) { throw new Error( `The value received from [${vcUrl}] is not a Verifiable Credential`, ); From cd4eef8842e9187563e835cdd73fdf7241dcdda4 Mon Sep 17 00:00:00 2001 From: Nicolas Ayral Seydoux Date: Mon, 4 Dec 2023 16:04:10 +0100 Subject: [PATCH 63/79] Cleanup flow typing --- src/common/common.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/common/common.ts b/src/common/common.ts index 94092704..2b8a717f 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -528,6 +528,14 @@ export async function verifiableCredentialToDataset( return internal_applyDataset(vc as { id: string }, store, options); } +function hasId(vc: unknown): vc is { id: string } { + return ( + typeof vc === "object" && + vc !== null && + typeof (vc as { id: unknown }).id === "string" + ); +} + // eslint-disable-next-line camelcase export async function internal_getVerifiableCredentialFromResponse( vcUrl: UrlString | undefined, @@ -613,20 +621,12 @@ export async function internal_getVerifiableCredentialFromResponse( }); } - if ( - // This is needed to make typescript happy; - // the compiler infers the type to be `null | undefined | {}` when it reaches this line. - typeof vc !== "object" || - vc === null || - !("id" in vc) || - typeof vc.id !== "string" - ) { + if (!hasId(vc)) { throw new Error( "Verifiable credential is not an object, or does not have an id", ); } - - const parsedVc = await verifiableCredentialToDataset(vc as { id: string }, { + const parsedVc = await verifiableCredentialToDataset(vc, { allowContextFetching: options.allowContextFetching, baseIRI: options.baseIRI, contexts: options.contexts, From 0f9f302cb785b712d65573ef2a3c92e3f1754f56 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:16:49 +0000 Subject: [PATCH 64/79] Update src/common/getters.ts Co-authored-by: Zwifi --- src/common/getters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/getters.ts b/src/common/getters.ts index a3398212..9976517c 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -127,7 +127,7 @@ export function getIssuanceDate(vc: DatasetWithId): Date { * ``` * * @param vc The Verifiable Credential - * @returns The expiration date + * @returns The expiration date, or undefined if none is found. */ export function getExpirationDate(vc: DatasetWithId): Date | undefined { const res = [ From 811cd518bba10ed671329af4a46d3d2535ff3e07 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:26:48 +0000 Subject: [PATCH 65/79] Update src/common/common.test.ts Co-authored-by: Zwifi --- src/common/common.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 215d6dc5..11bb519b 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -62,7 +62,7 @@ jest.mock("@inrupt/universal-fetch", () => { }); describe("normalizeVc", () => { - it("returns the same object", () => { + it("returns the same object when normalization is impossible", () => { const obj = {}; expect(normalizeVc(obj)).toEqual(obj); }); From 45b57240240a02b6a3d666c4d8b524003763d5ca Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:48:01 +0000 Subject: [PATCH 66/79] chore: use it.each in common.test.ts --- src/common/common.test.ts | 851 ++++++++++++++++++-------------------- 1 file changed, 398 insertions(+), 453 deletions(-) diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 11bb519b..9171ce69 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -523,29 +523,21 @@ describe("getVerifiableCredential", () => { ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; }); - it("returnLegacyJsonld: true", async () => { - await getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }, - ); - expect(mockedFetch).toHaveBeenCalledWith( - "https://example.org/ns/someCredentialInstance", - ); - }); - it("returnLegacyJsonld: false", async () => { - await getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }, - ); - expect(mockedFetch).toHaveBeenCalledWith( - "https://example.org/ns/someCredentialInstance", - ); - }); + it.each([[true], [false]])( + "returnLegacyJsonld: %s", + async (returnLegacyJsonld) => { + await getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ); + expect(mockedFetch).toHaveBeenCalledWith( + "https://example.org/ns/someCredentialInstance", + ); + }, + ); }); describe("throws if the VC ID cannot be dereferenced", () => { @@ -562,31 +554,22 @@ describe("getVerifiableCredential", () => { ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; }); - it("returnLegacyJsonld: true", async () => { - await expect( - getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }, - ), - ).rejects.toThrow( - "Fetching the Verifiable Credential [https://example.org/ns/someCredentialInstance] failed: 401 Unauthenticated", - ); - }); - it("returnLegacyJsonld: false", async () => { - await expect( - getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }, - ), - ).rejects.toThrow( - "Fetching the Verifiable Credential [https://example.org/ns/someCredentialInstance] failed: 401 Unauthenticated", - ); - }); + it.each([[true], [false]])( + "returnLegacyJsonld: %s", + async (returnLegacyJsonld) => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ), + ).rejects.toThrow( + "Fetching the Verifiable Credential [https://example.org/ns/someCredentialInstance] failed: 401 Unauthenticated", + ); + }, + ); }); describe("throws if the dereferenced data is invalid JSON", () => { @@ -600,31 +583,22 @@ describe("getVerifiableCredential", () => { >; }); - it("returnLegacyJsonld: true", async () => { - await expect( - getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }, - ), - ).rejects.toThrow( - "Parsing the Verifiable Credential [https://example.org/ns/someCredentialInstance] as JSON failed: SyntaxError:", - ); - }); - it("returnLegacyJsonld: false", async () => { - await expect( - getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }, - ), - ).rejects.toThrow( - "Parsing the Verifiable Credential [https://example.org/ns/someCredentialInstance] as JSON failed: SyntaxError:", - ); - }); + it.each([[true], [false]])( + "returnLegacyJsonld: %s", + async (returnLegacyJsonld) => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ), + ).rejects.toThrow( + "Parsing the Verifiable Credential [https://example.org/ns/someCredentialInstance] as JSON failed: SyntaxError:", + ); + }, + ); }); describe("throws if the dereferenced data is not a VC", () => { @@ -638,31 +612,24 @@ describe("getVerifiableCredential", () => { ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; }); - it("returnLegacyJsonld: true", async () => { - await expect( - getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }, - ), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - }); - it("returnLegacyJsonld: false", async () => { - await expect( - getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }, - ), - ).rejects.toThrow( - "Verifiable credential is not an object, or does not have an id", - ); - }); + it.each([[true], [false]])( + "returnLegacyJsonld: %s", + async (returnLegacyJsonld) => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ), + ).rejects.toThrow( + returnLegacyJsonld + ? "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential" + : "Verifiable credential is not an object, or does not have an id", + ); + }, + ); }); it.skip("throws if the dereferenced data has an unsupported content type", async () => { @@ -679,273 +646,262 @@ describe("getVerifiableCredential", () => { ).rejects.toThrow(/unsupported Content-Type/); }); - it("throws if the dereferenced data is empty", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response(JSON.stringify({}), { - headers: new Headers([["content-type", "application/json"]]), - }), - ); - - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }), - ).rejects.toThrow( - "Verifiable credential is not an object, or does not have an id", - ); - }); - - it("throws if the vc is a blank node", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response( - JSON.stringify({ - "@type": "https://www.w3.org/2018/credentials#VerifiableCredential", - }), - { + it.each([[true], [false]])( + "throws if the dereferenced data is emptyreturnLegacyJsonld: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response(JSON.stringify({}), { headers: new Headers([["content-type", "application/json"]]), - }, - ), - ); - - await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", - ); - await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }), - ).rejects.toThrow( - "Verifiable credential is not an object, or does not have an id", - ); - }); - - it("throws if the vc has a type that is a literal", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response( - JSON.stringify({ - "@context": "https://www.w3.org/2018/credentials/v1", - "@id": "http://example.org/my/vc", - "http://www.w3.org/1999/02/22-rdf-syntax-ns#type": [ - { - "@id": - "https://www.w3.org/2018/credentials#VerifiableCredential", - }, - "str", - ], }), - { - headers: new Headers([["content-type", "application/json"]]), - }, - ), - ); - - await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", - ); - await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", - ); - }); + ); - it("throws if the dereferenced data has 2 vcs", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response( - JSON.stringify([ - mockDefaultCredential(), - mockDefaultCredential("http://example.org/mockVC2"), - ]), + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", { - headers: new Headers([["content-type", "application/json"]]), + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, }, ), - ); - - await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", - ); - await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }), - ).rejects.toThrow( - "Verifiable credential is not an object, or does not have an id", - ); - }); + ).rejects.toThrow( + returnLegacyJsonld + ? "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential" + : "Verifiable credential is not an object, or does not have an id", + ); + }, + ); + + it.each([[true], [false]])( + "throws if the vc is a blank node: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response( + JSON.stringify({ + "@type": + "https://www.w3.org/2018/credentials#VerifiableCredential", + }), + { + headers: new Headers([["content-type", "application/json"]]), + }, + ), + ); - it("throws if the dereferenced data has 2 proofs", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response(JSON.stringify(mockDefaultCredential2Proofs()), { - headers: new Headers([["content-type", "application/json"]]), + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, }), - ); - - await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", - ); - await expect( - getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }), - ).rejects.toThrow( - "The value received from [https://some.vc] is not a Verifiable Credential", - ); - }); - - it("throws if the date field is not a valid xsd:dateTime", async () => { - const mocked = mockDefaultCredential(); - mocked.issuanceDate = "http://example.org/not/a/date"; + ).rejects.toThrow( + returnLegacyJsonld + ? "The value received from [https://some.vc] is not a Verifiable Credential" + : "Verifiable credential is not an object, or does not have an id", + ); + }, + ); + + it.each([[true], [false]])( + "throws if the vc has a type that is a literal: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response( + JSON.stringify({ + "@context": "https://www.w3.org/2018/credentials/v1", + "@id": "http://example.org/my/vc", + "http://www.w3.org/1999/02/22-rdf-syntax-ns#type": [ + { + "@id": + "https://www.w3.org/2018/credentials#VerifiableCredential", + }, + "str", + ], + }), + { + headers: new Headers([["content-type", "application/json"]]), + }, + ), + ); - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response(JSON.stringify(mocked), { - headers: new Headers([["content-type", "application/json"]]), + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, }), - ); - - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - }); - - it("throws if the date field is a string", async () => { - const mocked = mockDefaultCredential(); - // @ts-expect-error issuanceDate is required on the VC type - delete mocked.issuanceDate; - mocked["https://www.w3.org/2018/credentials#issuanceDate"] = - "http://example.org/not/a/date"; + ).rejects.toThrow( + returnLegacyJsonld + ? "The value received from [https://some.vc] is not a Verifiable Credential" + : "Verifiable credential is not an object, or does not have an id", + ); + }, + ); + + it.each([[true], [false]])( + "throws if the dereferenced data has 2 vcs: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response( + JSON.stringify([ + mockDefaultCredential(), + mockDefaultCredential("http://example.org/mockVC2"), + ]), + { + headers: new Headers([["content-type", "application/json"]]), + }, + ), + ); - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response(JSON.stringify(mocked), { - headers: new Headers([["content-type", "application/json"]]), + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, }), - ); - - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - }); - - it("throws if the date field is an IRI", async () => { - const mocked = mockDefaultCredential(); - // @ts-expect-error issuanceDate is required on the VC type - delete mocked.issuanceDate; - mocked["https://www.w3.org/2018/credentials#issuanceDate"] = { - "@id": "http://example.org/not/a/date", - }; + ).rejects.toThrow( + returnLegacyJsonld + ? "The value received from [https://some.vc] is not a Verifiable Credential" + : "Verifiable credential is not an object, or does not have an id", + ); + }, + ); + + it.each([[true], [false]])( + "throws if the dereferenced data has 2 proofs: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response(JSON.stringify(mockDefaultCredential2Proofs()), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response(JSON.stringify(mocked), { - headers: new Headers([["content-type", "application/json"]]), + await expect( + getVerifiableCredential("https://some.vc", { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, }), - ); + ).rejects.toThrow( + "The value received from [https://some.vc] is not a Verifiable Credential", + ); + }, + ); + + it.each([[true], [false]])( + "throws if the date field is not a valid xsd:dateTime: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mocked = mockDefaultCredential(); + mocked.issuanceDate = "http://example.org/not/a/date"; + + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - }); + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ), + ).rejects.toThrow( + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + }, + ); + + it.each([[true], [false]])( + "throws if the date field is a string: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mocked = mockDefaultCredential(); + // @ts-expect-error issuanceDate is required on the VC type + delete mocked.issuanceDate; + mocked["https://www.w3.org/2018/credentials#issuanceDate"] = + "http://example.org/not/a/date"; + + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); - it("throws if the issuer is a string", async () => { - const mocked = mockDefaultCredential(); - // @ts-expect-error issuer is of type string on the VC type - mocked.issuer = { "@value": "my string" }; + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ), + ).rejects.toThrow( + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + }, + ); + + it.each([[true], [false]])( + "throws if the date field is an IRI: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mocked = mockDefaultCredential(); + // @ts-expect-error issuanceDate is required on the VC type + delete mocked.issuanceDate; + mocked["https://www.w3.org/2018/credentials#issuanceDate"] = { + "@id": "http://example.org/not/a/date", + }; - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response(JSON.stringify(mocked), { - headers: new Headers([["content-type", "application/json"]]), - }), - ); + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - }); + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ), + ).rejects.toThrow( + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + }, + ); + + it.each([[true], [false]])( + "throws if the issuer is a string: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mocked = mockDefaultCredential(); + // @ts-expect-error issuer is of type string on the VC type + mocked.issuer = { "@value": "my string" }; + + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); + + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ), + ).rejects.toThrow( + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + }, + ); it("should handle credential subjects with multiple objects", async () => { const mocked = mockDefaultCredential(); @@ -1044,34 +1000,33 @@ describe("getVerifiableCredential", () => { expect(JSON.parse(JSON.stringify(vcNoLegacy))).toEqual(mocked); }); - it("throws if there are 2 proof values", async () => { - const mocked = mockDefaultCredential(); - // @ts-expect-error proofValue is a string not string[] in VC type - mocked.proof.proofValue = [mocked.proof.proofValue, "abc"]; + it.each([[true], [false]])( + "throws if there are 2 proof values", + async (returnLegacyJsonld) => { + const mocked = mockDefaultCredential(); + // @ts-expect-error proofValue is a string not string[] in VC type + mocked.proof.proofValue = [mocked.proof.proofValue, "abc"]; - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response(JSON.stringify(mocked), { - headers: new Headers([["content-type", "application/json"]]), - }), - ); + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response(JSON.stringify(mocked), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - await expect( - getVerifiableCredential("https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }), - ).rejects.toThrow( - "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", - ); - }); + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ), + ).rejects.toThrow( + "The value received from [https://example.org/ns/someCredentialInstance] is not a Verifiable Credential", + ); + }, + ); it("returns the fetched VC and the redirect URL", async () => { const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( @@ -1142,55 +1097,55 @@ describe("getVerifiableCredential", () => { ); }); - it("errors if the context contains an IRI that is not cached", async () => { - await expect( - getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }, - ), - ).rejects.toThrow( - "Unexpected context requested [http://example.org/my/sample/context]", - ); - }); - - it("errors if the context contains an IRI that is not cached [returnLegacyJsonld: false]", async () => { - await expect( - getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }, - ), - ).rejects.toThrow( - "Unexpected context requested [http://example.org/my/sample/context]", - ); - }); + it.each([[true], [false]])( + "errors if the context contains an IRI that is not cached: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ), + ).rejects.toThrow( + "Unexpected context requested [http://example.org/my/sample/context]", + ); + }, + ); - it("resolves if allowContextFetching is enabled and the context can be fetched", async () => { - (uniFetch as jest.Mock).mockResolvedValueOnce( - new Response( - JSON.stringify({ - "@context": {}, - }), - { - headers: new Headers([["content-type", "application/ld+json"]]), - }, - ), - ); + it.each([[true], [false]])( + "resolves if allowContextFetching is enabled and the context can be fetched: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + (uniFetch as jest.Mock).mockResolvedValueOnce( + new Response( + JSON.stringify({ + "@context": {}, + }), + { + headers: new Headers([["content-type", "application/ld+json"]]), + }, + ), + ); - await expect( - getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - allowContextFetching: true, - }, - ), - ).resolves.toMatchObject(mockCredential); - }); + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + allowContextFetching: true, + returnLegacyJsonld, + }, + ), + ).resolves.toMatchObject( + returnLegacyJsonld + ? mockCredential + : { + id: "https://example.org/ns/someCredentialInstance", + }, + ); + }, + ); it("can apply normalization of the response before parsing and returning it", async () => { (uniFetch as jest.Mock).mockResolvedValueOnce( @@ -1261,41 +1216,31 @@ describe("getVerifiableCredential", () => { }); }); - it("resolves if the context is cached", async () => { - await expect( - getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - allowContextFetching: true, - contexts: { - "http://example.org/my/sample/context": { - "@context": {}, + it.each([[true], [false]])( + "resolves if the context is cached: [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + await expect( + getVerifiableCredential( + "https://example.org/ns/someCredentialInstance", + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + allowContextFetching: true, + returnLegacyJsonld, + contexts: { + "http://example.org/my/sample/context": { + "@context": {}, + }, }, }, - }, - ), - ).resolves.toMatchObject(mockCredential); - }); - - it("resolves if the context is cached [returnLegacyJsonld: false]", async () => { - await expect( - getVerifiableCredential( - "https://example.org/ns/someCredentialInstance", - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - allowContextFetching: true, - returnLegacyJsonld: false, - contexts: { - "http://example.org/my/sample/context": { - "@context": {}, + ), + ).resolves.toMatchObject( + returnLegacyJsonld + ? mockCredential + : { + id: "https://example.org/ns/someCredentialInstance", }, - }, - }, - ), - ).resolves.toMatchObject({ - id: "https://example.org/ns/someCredentialInstance", - }); - }); + ); + }, + ); }); }); From 841d258b9c929e2d33924d6f8a90112b15b6897b Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:01:04 +0000 Subject: [PATCH 67/79] chore: comment on skipped content-type test and remove unused dependency --- package-lock.json | 9 +-------- package.json | 2 -- src/common/common.test.ts | 2 ++ 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index ded3c159..8b9cd004 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@inrupt/solid-client": "^1.25.2", "@inrupt/universal-fetch": "^1.0.1", - "content-type": "^1.0.5", "event-emitter-promisify": "^1.1.0", "jsonld-context-parser": "^2.4.0", "jsonld-streaming-parser": "^3.3.0", @@ -29,7 +28,6 @@ "@rdfjs/dataset": "2.0.1", "@rdfjs/types": "^1.1.0", "@rushstack/eslint-patch": "^1.1.4", - "@types/content-type": "^1.1.5", "@types/dotenv-flow": "^3.1.1", "@types/jest": "^29.2.2", "@types/md5": "^2.3.4", @@ -1840,12 +1838,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/content-type": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.8.tgz", - "integrity": "sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg==", - "dev": true - }, "node_modules/@types/dotenv-flow": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/dotenv-flow/-/dotenv-flow-3.3.1.tgz", @@ -3150,6 +3142,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "engines": { "node": ">= 0.6" } diff --git a/package.json b/package.json index c8d4bd92..0e172098 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "@rdfjs/dataset": "2.0.1", "@rdfjs/types": "^1.1.0", "@rushstack/eslint-patch": "^1.1.4", - "@types/content-type": "^1.1.5", "@types/dotenv-flow": "^3.1.1", "@types/jest": "^29.2.2", "@types/md5": "^2.3.4", @@ -89,7 +88,6 @@ "dependencies": { "@inrupt/solid-client": "^1.25.2", "@inrupt/universal-fetch": "^1.0.1", - "content-type": "^1.0.5", "event-emitter-promisify": "^1.1.0", "jsonld-context-parser": "^2.4.0", "jsonld-streaming-parser": "^3.3.0", diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 9171ce69..8d78c027 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -632,6 +632,8 @@ describe("getVerifiableCredential", () => { ); }); + // TODO: Enable this when we add content type checks in the next major version + // see https://github.com/inrupt/solid-client-vc-js/pull/849#discussion_r1414124524 it.skip("throws if the dereferenced data has an unsupported content type", async () => { const mockedFetch = jest .fn<(typeof UniversalFetch)["fetch"]>() From 66261b85b067fab25835a109392c3d398972b0ef Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 5 Dec 2023 00:50:12 +0000 Subject: [PATCH 68/79] chore: make query properly use dataset --- e2e/node/e2e.test.ts | 4 - src/common/common.mock.ts | 55 +++++ src/common/common.test.ts | 2 +- src/common/common.ts | 33 ++- src/lookup/derive.test.ts | 412 ++++++++++++++++---------------------- src/lookup/derive.ts | 2 +- src/lookup/query.test.ts | 301 ++++++++++++++++++---------- src/lookup/query.ts | 92 +++++++-- 8 files changed, 519 insertions(+), 382 deletions(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index a2d77820..5702dbc1 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -18,10 +18,6 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // - -// FIXME: Remove when refactoring to test matrix -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - import { getAuthenticatedSession, getNodeTestingEnvironment, diff --git a/src/common/common.mock.ts b/src/common/common.mock.ts index c344d835..3ac3e40b 100644 --- a/src/common/common.mock.ts +++ b/src/common/common.mock.ts @@ -166,3 +166,58 @@ export const mockDefaultPresentation = ( defaultVerifiableClaims, ) as VerifiablePresentation; }; + +export const mockAnonymousDefaultPresentation = ( + vc: VerifiableCredentialBase[] = [mockDefaultCredential()], +): VerifiablePresentation => { + return mockPartialPresentation(vc, { + ...defaultVerifiableClaims, + id: undefined, + }) as VerifiablePresentation; +}; + +export const mockAccessGrant = () => ({ + "@context": ["https://www.w3.org/2018/credentials/v1"], + type: "VerifiablePresentation", + holder: "https://vc.inrupt.com", + verifiableCredential: [ + { + id: "https://example.org/ns/someCredentialInstance", + type: ["SolidAccessRequest", "VerifiableCredential"], + proof: { + type: "Ed25519Signature2020", + created: "2023-12-05T00:11:29.159Z", + domain: "solid", + proofPurpose: "assertionMethod", + proofValue: "z4pPdpe9iQyFm2opvCJeoiW61Kajx8LqZQUFYLd", + verificationMethod: "https://example.org/verificationMethod/keys/1", + }, + credentialStatus: { + id: "https://vc.inrupt.com/status/At3i#0", + type: "RevocationList2020Status", + revocationListCredential: "https://vc.inrupt.com/status/At3i", + revocationListIndex: "0", + }, + credentialSubject: { + id: "https://some.webid.provider/strelka", + hasConsent: { + mode: "Read", + forPersonalData: "https://example.org/another-resource", + forPurpose: "http://example.org/some/purpose/1701735088943", + hasStatus: "ConsentStatusRequested", + isConsentForDataSubject: "https://some.webid/resource-owner", + }, + }, + issuanceDate: "1960-08-19T16:08:31Z", + issuer: "https://some.vc.issuer/in-ussr", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.inrupt.com/credentials/v1.jsonld", + "https://w3id.org/security/data-integrity/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://w3id.org/vc/status-list/2021/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + ], + }, + ], +}); diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 8d78c027..735b7f94 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -632,7 +632,7 @@ describe("getVerifiableCredential", () => { ); }); - // TODO: Enable this when we add content type checks in the next major version + // FIXME: Enable this when we add content type checks in the next major version // see https://github.com/inrupt/solid-client-vc-js/pull/849#discussion_r1414124524 it.skip("throws if the dereferenced data has an unsupported content type", async () => { const mockedFetch = jest diff --git a/src/common/common.ts b/src/common/common.ts index 2b8a717f..b62f7ebb 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -447,16 +447,17 @@ export async function getVerifiableCredentialApiConfiguration( } // eslint-disable-next-line camelcase -export function internal_applyDataset( +export function internal_applyDataset( vc: T, store: DatasetCore, options?: ParseOptions & { includeVcProperties?: boolean; additionalProperties?: Record; + requireId?: boolean; }, -): DatasetWithId { +): DatasetCore { return Object.freeze({ - id: vc.id, + ...(options?.requireId !== false && { id: vc.id }), ...(options?.includeVcProperties && vc), ...options?.additionalProperties, // Make this a DatasetCore without polluting the object with @@ -494,20 +495,42 @@ export async function verifiableCredentialToDataset( vc: T, options?: ParseOptions & { includeVcProperties: true; + additionalProperties?: Record; + requireId?: true; }, ): Promise; export async function verifiableCredentialToDataset( vc: T, options?: ParseOptions & { includeVcProperties?: boolean; + additionalProperties?: Record; + requireId?: true; }, ): Promise; +export async function verifiableCredentialToDataset( + vc: T, + options: ParseOptions & { + includeVcProperties: true; + additionalProperties?: Record; + requireId: false; + }, +): Promise; +export async function verifiableCredentialToDataset( + vc: T, + options: ParseOptions & { + includeVcProperties?: boolean; + additionalProperties?: Record; + requireId: false; + }, +): Promise; export async function verifiableCredentialToDataset( vc: T, options?: ParseOptions & { includeVcProperties?: boolean; + additionalProperties?: Record; + requireId?: boolean; }, -): Promise { +): Promise { let store: DatasetCore; try { store = await jsonLdToStore(vc, options); @@ -517,7 +540,7 @@ export async function verifiableCredentialToDataset( ); } - if (typeof vc.id !== "string") { + if (options?.requireId !== false && typeof vc.id !== "string") { throw new Error( `Expected vc.id to be a string, found [${ vc.id diff --git a/src/lookup/derive.test.ts b/src/lookup/derive.test.ts index 740d6e67..c673703f 100644 --- a/src/lookup/derive.test.ts +++ b/src/lookup/derive.test.ts @@ -19,16 +19,18 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import { jest, describe, it, expect } from "@jest/globals"; -import { Response } from "@inrupt/universal-fetch"; import type * as UniversalFetch from "@inrupt/universal-fetch"; -import { mockDefaultPresentation } from "../common/common.mock"; +import { Response } from "@inrupt/universal-fetch"; +import { describe, expect, it, jest } from "@jest/globals"; +import { + mockAccessGrant, + mockDefaultPresentation, +} from "../common/common.mock"; +import { getCredentialSubject } from "../common/getters"; import defaultGetVerifilableCredentialAllFromShape, { getVerifiableCredentialAllFromShape, } from "./derive"; import type * as QueryModule from "./query"; -import type { VerifiableCredential } from "../common/common"; -import { getCredentialSubject } from "../common/getters"; jest.mock("@inrupt/universal-fetch", () => { const fetchModule = jest.requireActual( @@ -40,22 +42,70 @@ jest.mock("@inrupt/universal-fetch", () => { }; }); -const mockDeriveEndpointDefaultResponse = () => - new Response(JSON.stringify(mockDefaultPresentation()), { - status: 200, - statusText: "OK", - }); +const mockDeriveEndpointDefaultResponse = (anoymous = false) => + new Response( + JSON.stringify(anoymous ? mockAccessGrant() : mockDefaultPresentation()), + { + status: 200, + statusText: "OK", + }, + ); -describe("getVerifiableCredentialAllFromShape", () => { +describe.each([ + ["named response", mockDeriveEndpointDefaultResponse], + ["anonymous response", () => mockDeriveEndpointDefaultResponse(true)], +])("getVerifiableCredentialAllFromShape [%s]", (_, mockResponse) => { describe("legacy derive endpoint", () => { it("exposes a default export", async () => { expect(defaultGetVerifilableCredentialAllFromShape).toBe( getVerifiableCredentialAllFromShape, ); }); - it("uses the provided fetch if any", async () => { - const mockedFetch = jest.fn() as typeof fetch; - try { + + it.each([[true], [false]])( + "uses the provided fetch if any [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest.fn() as typeof fetch; + try { + await getVerifiableCredentialAllFromShape( + "https://some.endpoint", + { + "@context": ["https://some.context"], + credentialSubject: { id: "https://some.subject/" }, + }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ); + // eslint-disable-next-line no-empty + } catch (_e) {} + expect(mockedFetch).toHaveBeenCalled(); + }, + ); + + it.each([[true], [false]])( + "defaults to an unauthenticated fetch if no fetch is provided [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest.requireMock( + "@inrupt/universal-fetch", + ) as jest.Mocked; + mockedFetch.fetch.mockResolvedValue(mockResponse()); + await getVerifiableCredentialAllFromShape("https://some.endpoint", { + "@context": ["https://some.context"], + credentialSubject: { id: "https://some.subject/" }, + returnLegacyJsonld, + }); + expect(mockedFetch.fetch).toHaveBeenCalled(); + }, + ); + + it.each([[true], [false]])( + "includes the expired VC options if requested [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValue(mockResponse()); await getVerifiableCredentialAllFromShape( "https://some.endpoint", { @@ -63,17 +113,28 @@ describe("getVerifiableCredentialAllFromShape", () => { credentialSubject: { id: "https://some.subject/" }, }, { + includeExpiredVc: true, fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, }, ); - // eslint-disable-next-line no-empty - } catch (_e) {} - expect(mockedFetch).toHaveBeenCalled(); - }); + expect(mockedFetch).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + body: expect.stringContaining("ExpiredVerifiableCredential"), + }), + ); + }, + ); - it("uses the provided fetch if any [returnLegacyJsonld: false]", async () => { - const mockedFetch = jest.fn() as typeof fetch; - try { + it.each([[true], [false]])( + "builds a legacy VP request from the provided VC shape", + async (returnLegacyJsonld) => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValue(mockResponse()); + const queryModule = jest.requireActual("./query") as typeof QueryModule; + const spiedQuery = jest.spyOn(queryModule, "query"); await getVerifiableCredentialAllFromShape( "https://some.endpoint", { @@ -82,156 +143,29 @@ describe("getVerifiableCredentialAllFromShape", () => { }, { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, + returnLegacyJsonld, }, ); - // eslint-disable-next-line no-empty - } catch (_e) {} - expect(mockedFetch).toHaveBeenCalled(); - }); - - it("defaults to an unauthenticated fetch if no fetch is provided", async () => { - const mockedFetch = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; - mockedFetch.fetch.mockResolvedValue(mockDeriveEndpointDefaultResponse()); - await getVerifiableCredentialAllFromShape("https://some.endpoint", { - "@context": ["https://some.context"], - credentialSubject: { id: "https://some.subject/" }, - }); - expect(mockedFetch.fetch).toHaveBeenCalled(); - }); - - it("defaults to an unauthenticated fetch if no fetch is provided [returnLegacyJsonld: false]", async () => { - const mockedFetch = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; - mockedFetch.fetch.mockResolvedValue(mockDeriveEndpointDefaultResponse()); - await getVerifiableCredentialAllFromShape( - "https://some.endpoint", - { - "@context": ["https://some.context"], - credentialSubject: { id: "https://some.subject/" }, - }, - { - returnLegacyJsonld: false, - }, - ); - expect(mockedFetch.fetch).toHaveBeenCalled(); - }); - - it("includes the expired VC options if requested", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue(mockDeriveEndpointDefaultResponse()); - await getVerifiableCredentialAllFromShape( - "https://some.endpoint", - { - "@context": ["https://some.context"], - credentialSubject: { id: "https://some.subject/" }, - }, - { - includeExpiredVc: true, - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }, - ); - expect(mockedFetch).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - body: expect.stringContaining("ExpiredVerifiableCredential"), - }), - ); - }); - - it("includes the expired VC options if requested [returnLegacyJsonld: false]", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue(mockDeriveEndpointDefaultResponse()); - await getVerifiableCredentialAllFromShape( - "https://some.endpoint", - { - "@context": ["https://some.context"], - credentialSubject: { id: "https://some.subject/" }, - }, - { - includeExpiredVc: true, - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }, - ); - expect(mockedFetch).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - body: expect.stringContaining("ExpiredVerifiableCredential"), - }), - ); - }); - - it("builds a legacy VP request from the provided VC shape", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue(mockDeriveEndpointDefaultResponse()); - const queryModule = jest.requireActual("./query") as typeof QueryModule; - const spiedQuery = jest.spyOn(queryModule, "query"); - await getVerifiableCredentialAllFromShape( - "https://some.endpoint", - { - "@context": ["https://some.context"], - credentialSubject: { id: "https://some.subject/" }, - }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, - ); - expect(spiedQuery).toHaveBeenCalledWith( - "https://some.endpoint", - expect.objectContaining({ - verifiableCredential: { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://some.context", - ], - credentialSubject: { id: "https://some.subject/" }, - }, - }), - expect.anything(), - ); - }); - - it("builds a legacy VP request from the provided VC shape [returnLegacyJsonld: false]", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue(mockDeriveEndpointDefaultResponse()); - const queryModule = jest.requireActual("./query") as typeof QueryModule; - const spiedQuery = jest.spyOn(queryModule, "query"); - await getVerifiableCredentialAllFromShape( - "https://some.endpoint", - { - "@context": ["https://some.context"], - credentialSubject: { id: "https://some.subject/" }, - }, - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }, - ); - expect(spiedQuery).toHaveBeenCalledWith( - "https://some.endpoint", - expect.objectContaining({ - verifiableCredential: { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://some.context", - ], - credentialSubject: { id: "https://some.subject/" }, - }, - }), - expect.anything(), - ); - }); + expect(spiedQuery).toHaveBeenCalledWith( + "https://some.endpoint", + expect.objectContaining({ + verifiableCredential: { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://some.context", + ], + credentialSubject: { id: "https://some.subject/" }, + }, + }), + expect.anything(), + ); + }, + ); it("returns the VCs from the obtained VP on a successful response", async () => { const mockedFetch = jest .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue(mockDeriveEndpointDefaultResponse()); + .mockResolvedValue(mockResponse()); const vc = await getVerifiableCredentialAllFromShape( "https://some.endpoint", @@ -243,19 +177,18 @@ describe("getVerifiableCredentialAllFromShape", () => { ); expect(vc).toMatchObject( - mockDefaultPresentation() - .verifiableCredential as VerifiableCredential[], + (await mockResponse().json()).verifiableCredential, ); expect(JSON.parse(JSON.stringify(vc))).toEqual( - mockDefaultPresentation().verifiableCredential, + (await mockResponse().json()).verifiableCredential, ); }); it("returns the VCs from the obtained VP on a successful response [returnLegacyJsonld: false]", async () => { const mockedFetch = jest .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue(mockDeriveEndpointDefaultResponse()); + .mockResolvedValue(mockResponse()); const vc = await getVerifiableCredentialAllFromShape( "https://some.endpoint", @@ -270,8 +203,8 @@ describe("getVerifiableCredentialAllFromShape", () => { ); expect(vc).toMatchObject( - mockDefaultPresentation().verifiableCredential!.map((v) => ({ - id: v.id, + mockDefaultPresentation().verifiableCredential!.map(() => ({ + id: "https://example.org/ns/someCredentialInstance", })), ); @@ -282,85 +215,82 @@ describe("getVerifiableCredentialAllFromShape", () => { ); expect(JSON.parse(JSON.stringify(vc))).toEqual( - mockDefaultPresentation().verifiableCredential, + (await mockResponse().json()).verifiableCredential, ); }); - it("returns an empty array if the VP contains no VCs", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response( - JSON.stringify({ - ...mockDefaultPresentation(), - verifiableCredential: undefined, - }), + it.each([[true], [false]])( + "returns an empty array if the VP contains no VCs [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response( + JSON.stringify({ + ...mockDefaultPresentation(), + verifiableCredential: undefined, + }), + { + status: 200, + statusText: "OK", + }, + ), + ); + await expect( + getVerifiableCredentialAllFromShape( + "https://some.endpoint", + { + "@context": ["https://some.context"], + credentialSubject: { id: "https://some.subject/" }, + }, { - status: 200, - statusText: "OK", + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, }, ), - ); - await expect( - getVerifiableCredentialAllFromShape( - "https://some.endpoint", - { - "@context": ["https://some.context"], - credentialSubject: { id: "https://some.subject/" }, - }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, - ), - ).resolves.toEqual([]); - await expect( - getVerifiableCredentialAllFromShape( - "https://some.endpoint", - { - "@context": ["https://some.context"], - credentialSubject: { id: "https://some.subject/" }, - }, - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }, - ), - ).resolves.toEqual([]); - }); + ).resolves.toEqual([]); + }, + ); }); describe("standard query endpoint", () => { - it("builds a standard VP request by example from the provided VC shape", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue(mockDeriveEndpointDefaultResponse()); - const queryModule = jest.requireActual("./query") as typeof QueryModule; - const spiedQuery = jest.spyOn(queryModule, "query"); - const VC_SHAPE = { - "@context": ["https://some.context"], - credentialSubject: { id: "https://some.subject/" }, - }; - await getVerifiableCredentialAllFromShape( - "https://some.endpoint/query", - VC_SHAPE, - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - }, - ); + it.each([[true], [false]])( + "builds a standard VP request by example from the provided VC shape [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValue(mockResponse()); + const queryModule = jest.requireActual("./query") as typeof QueryModule; + const spiedQuery = jest.spyOn(queryModule, "query"); + const VC_SHAPE = { + "@context": ["https://some.context"], + credentialSubject: { id: "https://some.subject/" }, + }; + await getVerifiableCredentialAllFromShape( + "https://some.endpoint/query", + VC_SHAPE, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ); - expect(spiedQuery).toHaveBeenCalledWith( - "https://some.endpoint/query", - expect.objectContaining({ - query: [ - { - type: "QueryByExample", - credentialQuery: [ - { - example: VC_SHAPE, - }, - ], - }, - ], - }), - expect.anything(), - ); - }); + expect(spiedQuery).toHaveBeenCalledWith( + "https://some.endpoint/query", + expect.objectContaining({ + query: [ + { + type: "QueryByExample", + credentialQuery: [ + { + example: VC_SHAPE, + }, + ], + }, + ], + }), + expect.anything(), + ); + }, + ); }); }); diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index 70853b5f..2f7a0ef5 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -199,7 +199,7 @@ export async function getVerifiableCredentialAllFromShape( returnLegacyJsonld: options?.returnLegacyJsonld, normalize: options?.normalize, }); - return vp.verifiableCredential ?? []; + return vp.verifiableCredential; } export default getVerifiableCredentialAllFromShape; diff --git a/src/lookup/query.test.ts b/src/lookup/query.test.ts index fc77591b..ae87ca1f 100644 --- a/src/lookup/query.test.ts +++ b/src/lookup/query.test.ts @@ -22,14 +22,19 @@ import { jest, it, describe, expect, beforeEach } from "@jest/globals"; import { Response } from "@inrupt/universal-fetch"; import type * as UniversalFetch from "@inrupt/universal-fetch"; +import { DataFactory } from "n3"; import type { QueryByExample } from "./query"; import { query } from "./query"; import { defaultVerifiableClaims, + mockAccessGrant, mockDefaultCredential, mockDefaultPresentation, mockPartialPresentation, } from "../common/common.mock"; +import { cred, rdf } from "../common/constants"; + +const { namedNode } = DataFactory; jest.mock("@inrupt/universal-fetch", () => { const fetchModule = jest.requireActual( @@ -134,7 +139,7 @@ describe("query", () => { }); }); - it.each([ + describe.each([ ["null", [null]], ["string", ["not a VC"]], ["empty object", [{}]], @@ -149,7 +154,7 @@ describe("query", () => { ], ])( "errors if the presentation contains invalid verifiable credentials [%s]", - async (_, elems) => { + (_, elems) => { const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( async () => // @ts-expect-error we are intentionall passing invalid VC types here to test for errors @@ -158,143 +163,205 @@ describe("query", () => { }), ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; - await expect( + it.each([[true], [false]])( + "[returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + await expect( + query( + "https://some.endpoint/query", + { query: [mockRequest] }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ), + ).rejects.toThrow(); + }, + ); + }, + ); + + it.each([ + [{ returnLegacyJsonld: true }], + [{ returnLegacyJsonld: false }], + [undefined], + ])( + "defaults to an unauthenticated fetch if no fetch is provided [args: %s]", + async (arg?: { returnLegacyJsonld?: boolean }) => { + const mockedFetch = jest.requireMock( + "@inrupt/universal-fetch", + ) as jest.Mocked; + mockedFetch.fetch.mockResolvedValueOnce( + new Response(JSON.stringify(mockDefaultPresentation()), { + status: 200, + }), + ); + await query( + "https://some.endpoint/query", + { query: [mockRequest] }, + arg, + ); + expect(mockedFetch.fetch).toHaveBeenCalled(); + }, + ); + + it.each([[true], [false]])( + "throws if the given endpoint returns an error [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response(undefined, { + status: 404, + }), + ); + await expect(() => query( - "https://some.endpoint/query", + "https://example.org/query", { query: [mockRequest] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, ), ).rejects.toThrow(); - await expect( + }, + ); + + it.each([[true], [false]])( + "throws if the endpoint responds with a non-JSON payload [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + async () => + new Response("Not JSON", { + status: 200, + }), + ); + await expect(() => query( - "https://some.endpoint/query", + "https://example.org/query", { query: [mockRequest] }, { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, + returnLegacyJsonld, }, ), ).rejects.toThrow(); }, ); - it("defaults to an unauthenticated fetch if no fetch is provided", async () => { - const mockedFetch = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; - mockedFetch.fetch.mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultPresentation()), { - status: 200, - }), - ); - await query("https://some.endpoint/query", { query: [mockRequest] }); - expect(mockedFetch.fetch).toHaveBeenCalled(); - }); + it.each([[true], [false]])( + "throws if the endpoint responds with a non-VP payload [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + await expect(() => + query( + "https://example.org/query", + { query: [mockRequest] }, + { + fetch: async () => + new Response(JSON.stringify({ json: "but not a VP" }), { + status: 200, + }), + returnLegacyJsonld, + }, + ), + ).rejects.toThrow(); + }, + ); - it("throws if the given endpoint returns an error", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response(undefined, { - status: 404, - }), - ); - await expect(() => - query( - "https://example.org/query", - { query: [mockRequest] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, - ), - ).rejects.toThrow(); - await expect(() => - query( - "https://example.org/query", + it.each([[true], [false]])( + "posts a request with the appropriate media type [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify(mockDefaultPresentation()), { + status: 200, + }), + ); + await query( + "https://some.endpoint/query", { query: [mockRequest] }, { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, + returnLegacyJsonld, }, - ), - ).rejects.toThrow(); - }); + ); + expect(mockedFetch).toHaveBeenCalledWith( + "https://some.endpoint/query", + expect.objectContaining({ + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }), + ); + }, + ); - it("throws if the endpoint responds with a non-JSON payload", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response("Not JSON", { - status: 200, + it.each([[true], [false]])( + "errors if no presentations exist [returnLegacyJsonld: %s]", + async (returnLegacyJsonld) => { + const mockedFetch = jest + .fn<(typeof UniversalFetch)["fetch"]>() + .mockResolvedValueOnce( + new Response(JSON.stringify([]), { + status: 200, + }), + ); + await expect( + query( + "https://some.endpoint/query", + { query: [mockRequest] }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld, + }, + ), + ).rejects.toThrow(); + expect(mockedFetch).toHaveBeenCalledWith( + "https://some.endpoint/query", + expect.objectContaining({ + headers: { + "Content-Type": "application/json", + }, + method: "POST", }), - ); - await expect(() => - query( - "https://example.org/query", - { query: [mockRequest] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, - ), - ).rejects.toThrow(); - await expect(() => - query( - "https://example.org/query", - { query: [mockRequest] }, - { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], - returnLegacyJsonld: false, - }, - ), - ).rejects.toThrow(); - }); + ); + }, + ); - it("throws if the endpoint responds with a non-VP payload", async () => { + it("returns the VP sent by the endpoint", async () => { const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( async () => - new Response(JSON.stringify({ json: "but not a VP" }), { + new Response(JSON.stringify(mockDefaultPresentation()), { status: 200, }), ); - await expect(() => - query( - "https://example.org/query", - { query: [mockRequest] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, - ), - ).rejects.toThrow(); - // FIXME: Re-enable this - // await expect(() => - // query( - // "https://example.org/query", - // { query: [mockRequest] }, - // { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], returnLegacyJsonld: false }, - // ), - // ).rejects.toThrow(); - }); - it("posts a request with the appropriate media type", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultPresentation()), { - status: 200, - }), - ); - await query( - "https://some.endpoint/query", + const vp = await query( + "https://example.org/query", { query: [mockRequest] }, { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, ); - expect(mockedFetch).toHaveBeenCalledWith( - "https://some.endpoint/query", - expect.objectContaining({ - headers: { - "Content-Type": "application/json", - }, - method: "POST", - }), + const vpNoLegacy = await query( + "https://example.org/query", + { query: [mockRequest] }, + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + returnLegacyJsonld: false, + }, + ); + expect(vp).toMatchObject(mockDefaultPresentation()); + expect(JSON.parse(JSON.stringify(vp))).toEqual(mockDefaultPresentation()); + expect(JSON.parse(JSON.stringify(vpNoLegacy))).toEqual( + mockDefaultPresentation(), ); }); - it("returns the VP sent by the endpoint", async () => { + it("returns the VP sent by the endpoint [using mock access grant]", async () => { const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( async () => - new Response(JSON.stringify(mockDefaultPresentation()), { + new Response(JSON.stringify(mockAccessGrant()), { status: 200, }), ); @@ -312,11 +379,29 @@ describe("query", () => { returnLegacyJsonld: false, }, ); - expect(vp).toMatchObject(mockDefaultPresentation()); - expect(JSON.parse(JSON.stringify(vp))).toEqual(mockDefaultPresentation()); - expect(JSON.parse(JSON.stringify(vpNoLegacy))).toEqual( - mockDefaultPresentation(), - ); + expect(vp).toMatchObject(mockAccessGrant()); + expect(JSON.parse(JSON.stringify(vp))).toEqual(mockAccessGrant()); + expect(JSON.parse(JSON.stringify(vpNoLegacy))).toEqual(mockAccessGrant()); + + expect(vp.holder).toBe("https://vc.inrupt.com"); + expect(vp.type).toBe("VerifiablePresentation"); + // @ts-expect-error the `holder` property does not exist in the newer version of the API + expect(vpNoLegacy.holder).toBeUndefined(); + // @ts-expect-error the `type` property does not exist in the newer version of the API + expect(vpNoLegacy.type).toBeUndefined(); + + expect( + vp.match( + null, + rdf.type, + namedNode( + "https://www.w3.org/2018/credentials#VerifiablePresentation", + ), + ).size, + ).toBe(1); + expect( + vp.match(null, cred.holder, namedNode("https://vc.inrupt.com")).size, + ).toBe(1); }); it("normalizes the VP sent by the endpoint", async () => { diff --git a/src/lookup/query.ts b/src/lookup/query.ts index 8c979bc7..a5f07e8d 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -21,6 +21,7 @@ import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; import { DataFactory } from "n3"; +import type { BlankNode, DatasetCore, NamedNode } from "@rdfjs/types"; import isRdfjsVerifiableCredential from "../common/isRdfjsVerifiableCredential"; import type { Iri, @@ -35,8 +36,10 @@ import { verifiableCredentialToDataset, } from "../common/common"; import { type ParseOptions } from "../parser/jsonld"; +import isRdfjsVerifiablePresentation from "../common/isRdfjsVerifiablePresentation"; +import { cred, rdf } from "../common/constants"; -const { namedNode } = DataFactory; +const { namedNode, defaultGraph } = DataFactory; /** * Based on https://w3c-ccg.github.io/vp-request-spec/#query-by-example. @@ -72,8 +75,10 @@ export type VerifiablePresentationRequest = { domain?: string; }; -interface ParsedVerifiablePresentation extends VerifiablePresentation { - verifiableCredential?: VerifiableCredential[]; +interface ParsedVerifiablePresentation + extends VerifiablePresentation, + DatasetCore { + verifiableCredential: VerifiableCredential[]; } /** @@ -116,7 +121,7 @@ export async function query( returnLegacyJsonld: false; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }, -): Promise<{ verifiableCredential?: DatasetWithId[] }>; +): Promise<{ verifiableCredential?: DatasetWithId[] } & DatasetCore>; /** * @deprecated Use RDFJS API instead of relying on the JSON structure by setting `returnLegacyJsonld` to false */ @@ -143,19 +148,21 @@ export async function query( normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }>, ): Promise< - ParsedVerifiablePresentation | { verifiableCredential?: DatasetWithId[] } + | ParsedVerifiablePresentation + | ({ verifiableCredential: DatasetWithId[] } & DatasetCore) >; export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, - options?: ParseOptions & + options: ParseOptions & Partial<{ fetch: typeof fallbackFetch; returnLegacyJsonld?: boolean; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; - }>, + }> = {}, ): Promise< - ParsedVerifiablePresentation | { verifiableCredential?: DatasetWithId[] } + | ParsedVerifiablePresentation + | ({ verifiableCredential: DatasetWithId[] } & DatasetCore) > { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { @@ -232,21 +239,55 @@ export async function query( // } // All code below here should is deprecated - let data; + let data: VerifiablePresentation & DatasetCore; + let rawData: VerifiablePresentation; try { - data = await response.json(); + rawData = await response.json(); - if (options?.returnLegacyJsonld !== false) { - data = normalizeVp(data); + if (options.returnLegacyJsonld !== false) { + rawData = normalizeVp(rawData); } + data = (await verifiableCredentialToDataset( + rawData, + { + includeVcProperties: options?.returnLegacyJsonld !== false, + additionalProperties: + typeof rawData.id === "string" ? { id: rawData.id } : {}, + requireId: false, + // This is a lie depending on how returnLegacyJsonld is set + }, + )) as VerifiablePresentation & DatasetCore; } catch (e) { throw new Error( `The holder [${queryEndpoint}] did not return a valid JSON response: parsing failed with error ${e}`, ); } + + let subject: BlankNode | NamedNode; + if (typeof data.id === "string") { + subject = namedNode(data.id); + } else { + const presentations = [ + ...data.match( + null, + rdf.type, + cred.VerifiablePresentation, + defaultGraph(), + ), + ]; + if (presentations.length !== 1) { + throw new Error( + `Expected exactly one Verifiable Presentation. Found ${presentations.length}.`, + ); + } + + subject = presentations[0].subject as BlankNode | NamedNode; + } + if ( - options?.returnLegacyJsonld !== false && - !isVerifiablePresentation(data) + options.returnLegacyJsonld === false + ? !isRdfjsVerifiablePresentation(data, subject as BlankNode | NamedNode) + : !isVerifiablePresentation(data) ) { throw new Error( `The holder [${queryEndpoint}] did not return a Verifiable Presentation: ${JSON.stringify( @@ -255,15 +296,18 @@ export async function query( ); } - if (data.verifiableCredential && Array.isArray(data.verifiableCredential)) { - const newVerifiableCredential: Promise[] = []; - for (let i = 0; i < data.verifiableCredential.length; i += 100) { + const newVerifiableCredential: DatasetWithId[] = []; + if ( + rawData.verifiableCredential && + Array.isArray(rawData.verifiableCredential) + ) { + for (let i = 0; i < rawData.verifiableCredential.length; i += 100) { newVerifiableCredential.push( // Limit concurrency to avoid memory overflows. For details see // https://github.com/inrupt/solid-client-vc-js/pull/849#discussion_r1377400688 // eslint-disable-next-line no-await-in-loop ...(await Promise.all( - data.verifiableCredential + rawData.verifiableCredential .slice(i, i + 100) .map(async (_vc: VerifiableCredentialBase) => { let vc = _vc; @@ -271,13 +315,13 @@ export async function query( throw new Error(`Verifiable Credentail is an invalid object`); } - if (options?.normalize) { + if (options.normalize) { vc = options.normalize(vc); } const res = await verifiableCredentialToDataset(vc, { ...options, - includeVcProperties: options?.returnLegacyJsonld !== false, + includeVcProperties: options.returnLegacyJsonld !== false, }); // FIXME: Address the type issue here @@ -292,7 +336,11 @@ export async function query( )), ); } - data.verifiableCredential = newVerifiableCredential; } - return data as ParsedVerifiablePresentation; + return { + ...data, + verifiableCredential: newVerifiableCredential, + } as + | ParsedVerifiablePresentation + | ({ verifiableCredential: DatasetWithId[] } & DatasetCore); } From c098ef406a862d7742bb114f52d61b8486b3d4f7 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:37:49 +0000 Subject: [PATCH 69/79] chore: make verify functions rdfjs --- e2e/node/e2e.test.ts | 330 ++++++++++++++++---- src/common/common.ts | 6 +- src/common/isRdfjsVerifiableCredential.ts | 8 + src/common/isRdfjsVerifiablePresentation.ts | 36 +++ src/lookup/query.ts | 83 ++--- src/parser/jsonld.ts | 1 + src/verify/verify.test.ts | 233 ++++++++++++-- src/verify/verify.ts | 114 +++++-- 8 files changed, 639 insertions(+), 172 deletions(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index 5702dbc1..0e3d0024 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -23,7 +23,15 @@ import { getNodeTestingEnvironment, } from "@inrupt/internal-test-env"; import type { Session } from "@inrupt/solid-client-authn-node"; -import { describe, it, expect, beforeAll, afterAll } from "@jest/globals"; +import { + describe, + it, + expect, + beforeAll, + afterAll, + beforeEach, +} from "@jest/globals"; +import type { VerifiablePresentationRequest } from "../../src/index"; import { getCredentialSubject, getVerifiableCredentialAllFromShape, @@ -31,7 +39,12 @@ import { getVerifiableCredential, issueVerifiableCredential, revokeVerifiableCredential, + isValidVc, + getId, + query, + isValidVerifiablePresentation, } from "../../src/index"; +import { concatenateContexts, defaultContext } from "../../src/common/common"; const validCredentialClaims = { "@context": [ @@ -89,6 +102,17 @@ describe("End-to-end verifiable credentials tests for environment", () => { let derivationService: string; let statusService: string; let verifierService: string; + let revokedObject: { + errors: string[]; + }; + + beforeEach(() => { + revokedObject = { + errors: [ + "credentialStatus validation has failed: credential has been revoked", + ], + }; + }); beforeAll(async () => { session = await getAuthenticatedSession(env); @@ -130,42 +154,58 @@ describe("End-to-end verifiable credentials tests for environment", () => { describe("issue a VC", () => { it("Successfully gets a VC from a valid issuer", async () => { - const credential = await issueVerifiableCredential( - issuerService, - validSubjectClaims(), - validCredentialClaims, - { - fetch: session.fetch, - }, - ); + const [ + credential, + credentialWithSubject, + credentialWithSubjectNoLegacyJson, + credentialNoProperties, + ] = await Promise.all([ + issueVerifiableCredential( + issuerService, + validSubjectClaims(), + validCredentialClaims, + { + fetch: session.fetch, + }, + ), + issueVerifiableCredential( + issuerService, + "http://example.org/my/subject/id", + validSubjectClaims(), + validCredentialClaims, + + { + fetch: session.fetch, + }, + ), + issueVerifiableCredential( + issuerService, + "http://example.org/my/subject/id", + validSubjectClaims(), + validCredentialClaims, + + { + fetch: session.fetch, + returnLegacyJsonld: false, + }, + ), + issueVerifiableCredential( + issuerService, + validSubjectClaims(), + validCredentialClaims, + { + fetch: session.fetch, + returnLegacyJsonld: false, + }, + ), + ]); + expect(credential.credentialSubject.id).toBe(vcSubject); expect(getCredentialSubject(credential).value).toBe(vcSubject); - const credentialWithSubject = await issueVerifiableCredential( - issuerService, - "http://example.org/my/subject/id", - validSubjectClaims(), - validCredentialClaims, - - { - fetch: session.fetch, - }, - ); expect(credentialWithSubject.credentialSubject.id).toBe(vcSubject); expect(getCredentialSubject(credentialWithSubject).value).toBe(vcSubject); - const credentialWithSubjectNoLegacyJson = await issueVerifiableCredential( - issuerService, - "http://example.org/my/subject/id", - validSubjectClaims(), - validCredentialClaims, - - { - fetch: session.fetch, - returnLegacyJsonld: false, - }, - ); - expect( // @ts-expect-error the credentialSubject property should not exist if legacy json is disabled credentialWithSubjectNoLegacyJson.credentialSubject, @@ -174,40 +214,92 @@ describe("End-to-end verifiable credentials tests for environment", () => { getCredentialSubject(credentialWithSubjectNoLegacyJson).value, ).toBe(vcSubject); - const credentialNoProperties = await issueVerifiableCredential( - issuerService, - validSubjectClaims(), - validCredentialClaims, - { - fetch: session.fetch, - returnLegacyJsonld: false, - }, - ); - // @ts-expect-error the credentialSubject property should not exist if legacy json is disabled expect(credentialNoProperties.credentialSubject).toBeUndefined(); expect(getCredentialSubject(credentialNoProperties).value).toBe( vcSubject, ); - await Promise.all([ - revokeVerifiableCredential(statusService, credential.id, { - fetch: session.fetch, - }), - revokeVerifiableCredential(statusService, credentialWithSubject.id, { - fetch: session.fetch, - }), - revokeVerifiableCredential( - statusService, + await Promise.all( + [ + credential.id, + credentialWithSubject.id, credentialWithSubjectNoLegacyJson.id, + credentialNoProperties.id, + ].map((cred) => + revokeVerifiableCredential(statusService, cred, { + fetch: session.fetch, + }), + ), + ); + }); + + it("successfully performs isValidVc checks", async () => { + const purpose = `http://example.org/some/purpose/${Date.now()}`; + const [credential1, credential2] = await Promise.all([ + issueVerifiableCredential( + issuerService, + validSubjectClaims({ + resource: "https://example.org/some-resource", + purpose, + }), + validCredentialClaims, + { + fetch: session.fetch, + }, + ), + issueVerifiableCredential( + issuerService, + validSubjectClaims({ + resource: "https://example.org/another-resource", + purpose, + }), + validCredentialClaims, { fetch: session.fetch, + returnLegacyJsonld: false, }, ), - revokeVerifiableCredential(statusService, credentialNoProperties.id, { + ]); + + const creds = [ + credential1, + credential2, + credential1.id, + credential2.id, + getId(credential1), + getId(credential2), + ]; + await Promise.all( + creds.map((cred) => + expect( + isValidVc(cred, { + fetch: session.fetch, + verificationEndpoint: verifierService, + }), + ).resolves.toMatchObject({ errors: [] }), + ), + ); + + await Promise.all([ + revokeVerifiableCredential(statusService, credential1.id, { + fetch: session.fetch, + }), + revokeVerifiableCredential(statusService, credential2.id, { fetch: session.fetch, }), ]); + + await Promise.all( + creds.map((cred) => + expect( + isValidVc(cred, { + fetch: session.fetch, + verificationEndpoint: verifierService, + }), + ).resolves.toMatchObject(revokedObject), + ), + ); }); // FIXME: based on configuration, the server may have one of two behaviors @@ -274,7 +366,12 @@ describe("End-to-end verifiable credentials tests for environment", () => { }, }; - const [allDeprecated, allNew] = await Promise.all([ + const [ + allDeprecated, + allNew, + verifiablePresentationLegacy, + verifiablePresentation, + ] = await Promise.all([ getVerifiableCredentialAllFromShape(derivationService, matcher, { fetch: session.fetch, includeExpiredVc: false, @@ -284,10 +381,70 @@ describe("End-to-end verifiable credentials tests for environment", () => { includeExpiredVc: false, returnLegacyJsonld: false, }), + query( + derivationService, + { + verifiableCredential: { + ...matcher, + "@context": concatenateContexts( + defaultContext, + matcher["@context"], + ), + }, + } as unknown as VerifiablePresentationRequest, + { + fetch: session.fetch, + }, + ), + query( + derivationService, + { + verifiableCredential: { + ...matcher, + "@context": concatenateContexts( + defaultContext, + matcher["@context"], + ), + }, + } as unknown as VerifiablePresentationRequest, + { + fetch: session.fetch, + returnLegacyJsonld: false, + }, + ), + ]); + + await Promise.all([ + expect( + isValidVerifiablePresentation( + verifierService, + verifiablePresentationLegacy, + { + fetch: session.fetch, + }, + ), + ).resolves.toMatchObject({ errors: [] }), + expect( + isValidVerifiablePresentation( + verifierService, + verifiablePresentation, + { + fetch: session.fetch, + }, + ), + ).resolves.toMatchObject({ errors: [] }), ]); expect(allDeprecated).toHaveLength(2); + await expect( + Promise.all( + allDeprecated.map((dep) => + isValidVc(dep, { verificationEndpoint: verifierService }), + ), + ), + ).resolves.toMatchObject([{ errors: [] }, { errors: [] }]); + expect(allDeprecated[0].credentialSubject.id).toBe(vcSubject); expect(getCredentialSubject(allDeprecated[0]).value).toBe(vcSubject); expect(allDeprecated[1].credentialSubject.id).toBe(vcSubject); @@ -303,31 +460,38 @@ describe("End-to-end verifiable credentials tests for environment", () => { expect(allNew[1].credentialSubject).toBeUndefined(); expect(getCredentialSubject(allNew[1]).value).toBe(vcSubject); - await expect( + const [queriedCredential1Legacy, queriedCredential1] = await Promise.all([ getVerifiableCredentialAllFromShape(derivationService, credential1, { fetch: session.fetch, }), - ).resolves.toHaveLength(1); - - await expect( - getVerifiableCredentialAllFromShape(derivationService, credential2, { - fetch: session.fetch, - }), - ).resolves.toHaveLength(1); - - await expect( getVerifiableCredentialAllFromShape(derivationService, credential1, { fetch: session.fetch, returnLegacyJsonld: false, }), - ).resolves.toHaveLength(1); + ]); - await expect( - getVerifiableCredentialAllFromShape(derivationService, credential2, { - fetch: session.fetch, - returnLegacyJsonld: false, - }), - ).resolves.toHaveLength(1); + expect(queriedCredential1Legacy).toHaveLength(1); + expect(queriedCredential1).toHaveLength(1); + + await Promise.all([ + expect( + getVerifiableCredentialAllFromShape(derivationService, credential2, { + fetch: session.fetch, + }), + ).resolves.toHaveLength(1), + expect( + getVerifiableCredentialAllFromShape(derivationService, credential1, { + fetch: session.fetch, + returnLegacyJsonld: false, + }), + ).resolves.toHaveLength(1), + expect( + getVerifiableCredentialAllFromShape(derivationService, credential2, { + fetch: session.fetch, + returnLegacyJsonld: false, + }), + ).resolves.toHaveLength(1), + ]); const [credential1FetchedLegacy, credential1Fetched] = await Promise.all([ getVerifiableCredential(credential1.id, { @@ -347,6 +511,23 @@ describe("End-to-end verifiable credentials tests for environment", () => { vcSubject, ); + const creds = [ + credential1Fetched, + credential1FetchedLegacy, + queriedCredential1Legacy[0], + queriedCredential1[0], + ]; + await Promise.all( + creds.map((cred) => + expect( + isValidVc(cred, { + fetch: session.fetch, + verificationEndpoint: verifierService, + }), + ).resolves.toMatchObject({ errors: [] }), + ), + ); + await Promise.all([ revokeVerifiableCredential(statusService, credential1.id, { fetch: session.fetch, @@ -355,6 +536,17 @@ describe("End-to-end verifiable credentials tests for environment", () => { fetch: session.fetch, }), ]); + + await Promise.all( + creds.map((cred) => + expect( + isValidVc(cred, { + fetch: session.fetch, + verificationEndpoint: verifierService, + }), + ).resolves.toMatchObject(revokedObject), + ), + ); }, 60_000); }); diff --git a/src/common/common.ts b/src/common/common.ts index b62f7ebb..a3f8828a 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -517,10 +517,10 @@ export async function verifiableCredentialToDataset( ): Promise; export async function verifiableCredentialToDataset( vc: T, - options: ParseOptions & { + options?: ParseOptions & { includeVcProperties?: boolean; additionalProperties?: Record; - requireId: false; + requireId?: boolean; }, ): Promise; export async function verifiableCredentialToDataset( @@ -551,7 +551,7 @@ export async function verifiableCredentialToDataset( return internal_applyDataset(vc as { id: string }, store, options); } -function hasId(vc: unknown): vc is { id: string } { +export function hasId(vc: unknown): vc is { id: string } { return ( typeof vc === "object" && vc !== null && diff --git a/src/common/isRdfjsVerifiableCredential.ts b/src/common/isRdfjsVerifiableCredential.ts index e2fd1313..4e2dae68 100644 --- a/src/common/isRdfjsVerifiableCredential.ts +++ b/src/common/isRdfjsVerifiableCredential.ts @@ -26,6 +26,14 @@ import { isValidProof, isDate } from "./getters"; const { defaultGraph, quad } = DataFactory; +/** + * Verifies that a given JSON-LD payload conforms to the Verifiable Credential + * schema we expect. + * @param data The JSON-LD payload as an RDFJS dataset + * @param id The id of the VerifiableCredential as a Named Node + * @returns true is the payload matches our expectation. + * @deprecated Use isRdfjsVerifiableCredential instead + */ export default function isRdfjsVerifiableCredential( dataset: DatasetCore, id: NamedNode, diff --git a/src/common/isRdfjsVerifiablePresentation.ts b/src/common/isRdfjsVerifiablePresentation.ts index 9fda245c..75bdb492 100644 --- a/src/common/isRdfjsVerifiablePresentation.ts +++ b/src/common/isRdfjsVerifiablePresentation.ts @@ -26,6 +26,42 @@ import isRdfjsVerifiableCredential from "./isRdfjsVerifiableCredential"; const { defaultGraph } = DataFactory; +export function getHolder( + dataset: DatasetCore, + id: NamedNode | BlankNode, +): string { + const holder = [...dataset.match(id, cred.holder, null, defaultGraph())]; + if ( + holder.length === 1 && + holder[0].object.termType === "NamedNode" && + isUrl(holder[0].object.value) + ) { + return holder[0].object.value; + } + + throw new Error("Could not find a valid holder"); +} + +export function getVpSubject(data: DatasetCore) { + const presentations = [ + ...data.match(null, rdf.type, cred.VerifiablePresentation, defaultGraph()), + ]; + if (presentations.length !== 1) { + throw new Error( + `Expected exactly one Verifiable Presentation. Found ${presentations.length}.`, + ); + } + + const { subject } = presentations[0]; + if (subject.termType !== "BlankNode" && subject.termType !== "NamedNode") { + throw new Error( + `Expected VP subject to be NamedNode or BlankNode. Instead found [${subject.value}] with termType [${subject.termType}]`, + ); + } + + return subject; +} + export default function isRdfjsVerifiablePresentation( dataset: DatasetCore, id: NamedNode | BlankNode, diff --git a/src/lookup/query.ts b/src/lookup/query.ts index a5f07e8d..fe4eb571 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -20,26 +20,27 @@ // import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; +import type { DatasetCore } from "@rdfjs/types"; import { DataFactory } from "n3"; -import type { BlankNode, DatasetCore, NamedNode } from "@rdfjs/types"; -import isRdfjsVerifiableCredential from "../common/isRdfjsVerifiableCredential"; import type { + DatasetWithId, Iri, VerifiableCredential, VerifiableCredentialBase, VerifiablePresentation, - DatasetWithId, } from "../common/common"; import { isVerifiablePresentation, normalizeVp, verifiableCredentialToDataset, } from "../common/common"; +import isRdfjsVerifiableCredential from "../common/isRdfjsVerifiableCredential"; +import isRdfjsVerifiablePresentation, { + getVpSubject, +} from "../common/isRdfjsVerifiablePresentation"; import { type ParseOptions } from "../parser/jsonld"; -import isRdfjsVerifiablePresentation from "../common/isRdfjsVerifiablePresentation"; -import { cred, rdf } from "../common/constants"; -const { namedNode, defaultGraph } = DataFactory; +const { namedNode } = DataFactory; /** * Based on https://w3c-ccg.github.io/vp-request-spec/#query-by-example. @@ -75,12 +76,16 @@ export type VerifiablePresentationRequest = { domain?: string; }; -interface ParsedVerifiablePresentation +export interface ParsedVerifiablePresentation extends VerifiablePresentation, DatasetCore { verifiableCredential: VerifiableCredential[]; } +export type MinimalPresentation = { + verifiableCredential: DatasetWithId[]; +} & DatasetCore; + /** * Send a Verifiable Presentation Request to a query endpoint in order to retrieve * all Verifiable Credentials matching the query, wrapped in a single Presentation. @@ -117,23 +122,22 @@ export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, options: ParseOptions & { - fetch: typeof fallbackFetch; + fetch?: typeof fallbackFetch; returnLegacyJsonld: false; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }, -): Promise<{ verifiableCredential?: DatasetWithId[] } & DatasetCore>; +): Promise; /** * @deprecated Use RDFJS API instead of relying on the JSON structure by setting `returnLegacyJsonld` to false */ export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, - options?: ParseOptions & - Partial<{ - fetch: typeof fallbackFetch; - returnLegacyJsonld?: true; - normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; - }>, + options?: ParseOptions & { + fetch?: typeof fallbackFetch; + returnLegacyJsonld?: true; + normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; + }, ): Promise; /** * @deprecated Use RDFJS API instead of relying on the JSON structure by setting `returnLegacyJsonld` to false @@ -141,16 +145,12 @@ export async function query( export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, - options?: ParseOptions & - Partial<{ - fetch: typeof fallbackFetch; - returnLegacyJsonld?: boolean; - normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; - }>, -): Promise< - | ParsedVerifiablePresentation - | ({ verifiableCredential: DatasetWithId[] } & DatasetCore) ->; + options?: ParseOptions & { + fetch?: typeof fallbackFetch; + returnLegacyJsonld?: boolean; + normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; + }, +): Promise; export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, @@ -160,10 +160,7 @@ export async function query( returnLegacyJsonld?: boolean; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }> = {}, -): Promise< - | ParsedVerifiablePresentation - | ({ verifiableCredential: DatasetWithId[] } & DatasetCore) -> { +): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { internalOptions.fetch = fallbackFetch; @@ -250,7 +247,7 @@ export async function query( data = (await verifiableCredentialToDataset( rawData, { - includeVcProperties: options?.returnLegacyJsonld !== false, + includeVcProperties: options.returnLegacyJsonld !== false, additionalProperties: typeof rawData.id === "string" ? { id: rawData.id } : {}, requireId: false, @@ -263,30 +260,11 @@ export async function query( ); } - let subject: BlankNode | NamedNode; - if (typeof data.id === "string") { - subject = namedNode(data.id); - } else { - const presentations = [ - ...data.match( - null, - rdf.type, - cred.VerifiablePresentation, - defaultGraph(), - ), - ]; - if (presentations.length !== 1) { - throw new Error( - `Expected exactly one Verifiable Presentation. Found ${presentations.length}.`, - ); - } - - subject = presentations[0].subject as BlankNode | NamedNode; - } - + const subject = + typeof data.id === "string" ? namedNode(data.id) : getVpSubject(data); if ( options.returnLegacyJsonld === false - ? !isRdfjsVerifiablePresentation(data, subject as BlankNode | NamedNode) + ? !isRdfjsVerifiablePresentation(data, subject) : !isVerifiablePresentation(data) ) { throw new Error( @@ -324,7 +302,6 @@ export async function query( includeVcProperties: options.returnLegacyJsonld !== false, }); - // FIXME: Address the type issue here if (!isRdfjsVerifiableCredential(res, namedNode(res.id))) { throw new Error( `[${res.id}] is not a Valid Verifiable Credential`, diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index ab428309..ae219213 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -90,6 +90,7 @@ function hashContext( return typeof context === "string" ? md5(context) : cmap(context).toString(); } +// This is a workaround until https://github.com/rubensworks/jsonld-context-parser.js/pull/70 is closed export class CachingContextParser extends ContextParser { private cachedParsing: Record> = {}; diff --git a/src/verify/verify.test.ts b/src/verify/verify.test.ts index a8eaef94..5d221fc3 100644 --- a/src/verify/verify.test.ts +++ b/src/verify/verify.test.ts @@ -19,10 +19,12 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import { jest, describe, it, expect } from "@jest/globals"; -import { mocked } from "jest-mock"; -import { Response } from "@inrupt/universal-fetch"; import type * as UniversalFetch from "@inrupt/universal-fetch"; +import { Response } from "@inrupt/universal-fetch"; +import { describe, expect, it, jest } from "@jest/globals"; +import { mocked } from "jest-mock"; +import { DataFactory, Store } from "n3"; +import type * as Common from "../common/common"; import { getVerifiableCredential, getVerifiableCredentialApiConfiguration, @@ -30,8 +32,31 @@ import { isVerifiablePresentation, } from "../common/common"; import { isValidVc, isValidVerifiablePresentation } from "./verify"; +import { jsonLdStringToStore } from "../parser/jsonld"; +import { cred, rdf } from "../common/constants"; +import { + getHolder, + getVpSubject, +} from "../common/isRdfjsVerifiablePresentation"; + +const { quad, namedNode, literal } = DataFactory; -jest.mock("../common/common"); +jest.mock("../common/common", () => { + return { + verifiableCredentialToDataset: + jest.requireActual("../common/common") + .verifiableCredentialToDataset, + isVerifiablePresentation: jest.fn(), + isVerifiableCredential: jest.fn(), + getVerifiableCredential: jest.fn( + jest.requireActual("../common/common") + .getVerifiableCredential, + ), + isUrl: jest.requireActual("../common/common").isUrl, + hasId: jest.requireActual("../common/common").hasId, + getVerifiableCredentialApiConfiguration: jest.fn(), + }; +}); jest.mock("@inrupt/universal-fetch", () => { const fetchModule = jest.requireActual( "@inrupt/universal-fetch", @@ -45,7 +70,8 @@ jest.mock("@inrupt/universal-fetch", () => { const MOCK_VC = { "@context": [ "https://www.w3.org/2018/credentials/v1", - "https://consent.pod.inrupt.com/credentials/v1", + "https://schema.inrupt.com/credentials/v1.jsonld", + // "https://consent.pod.inrupt.com/credentials/v1", ], id: "https://example.com/id", issuer: "https://example.com/issuer", @@ -66,10 +92,11 @@ const MOCK_VC = { created: "2021-05-26T16:40:03.009Z", proofPurpose: "assertionMethod", proofValue: "eqp8h_kL1DwJCpn65z-d1Arnysx6b11...jb8j0MxUCc1uDQ", - type: "Ed25519Signature2020", + type: "Ed25519Signature2018", verificationMethod: "https://consent.pod.inrupt.com/key/396f686b", }, }; + describe("isValidVc", () => { const MOCK_VERIFY_ENDPOINT = "https://consent.example.com"; const MOCK_VERIFY_RESPONSE = { checks: [], warning: [], errors: [] }; @@ -78,7 +105,7 @@ describe("isValidVc", () => { const mockedFetch = jest.requireMock( "@inrupt/universal-fetch", ) as jest.Mocked; - mocked(isVerifiableCredential).mockReturnValueOnce(true); + mockedFetch.fetch.mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, @@ -115,7 +142,6 @@ describe("isValidVc", () => { legacy: {}, specCompliant: {}, }); - mocked(isVerifiableCredential).mockReturnValueOnce(true); await isValidVc(MOCK_VC); expect(mockedDiscovery).toHaveBeenCalledWith(MOCK_VC.issuer); @@ -127,7 +153,7 @@ describe("isValidVc", () => { status: 200, }), ); - mocked(isVerifiableCredential).mockReturnValueOnce(true); + await isValidVc(MOCK_VC, { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], verificationEndpoint: MOCK_VERIFY_ENDPOINT, @@ -136,13 +162,33 @@ describe("isValidVc", () => { expect(mockedFetch).toHaveBeenCalled(); }); + it("uses the provided fetch if any on RDFJS input", async () => { + const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { + status: 200, + }), + ); + + await isValidVc( + Object.assign(await jsonLdStringToStore(JSON.stringify(MOCK_VC)), { + id: MOCK_VC.id, + }), + { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + verificationEndpoint: MOCK_VERIFY_ENDPOINT, + }, + ); + + expect(mockedFetch).toHaveBeenCalled(); + }); + it("sends the given vc to the verify endpoint", async () => { mocked(getVerifiableCredentialApiConfiguration).mockResolvedValueOnce({ verifierService: "https://some.vc.verifier", legacy: {}, specCompliant: {}, }); - mocked(isVerifiableCredential).mockReturnValueOnce(true); + const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, @@ -174,7 +220,7 @@ describe("isValidVc", () => { status: 200, }), ); - mocked(isVerifiableCredential).mockReturnValueOnce(true); + await isValidVc("https://example.com/someVc", { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, @@ -201,7 +247,27 @@ describe("isValidVc", () => { verificationEndpoint: MOCK_VERIFY_ENDPOINT, }), ).rejects.toThrow( - "The request to [https://example.com/someVc] returned an unexpected response: undefined", + "Fetching the Verifiable Credential [https://example.com/someVc] failed: 400 Failed", + ); + }); + + it("throws if the VC does not have an ID", async () => { + const mockedFetch = jest + .fn(global.fetch) + .mockResolvedValueOnce( + new Response(undefined, { status: 400, statusText: "Failed" }), + ); + await expect( + isValidVc( + { ...MOCK_VC, id: undefined as unknown as string }, + { + fetch: + mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + verificationEndpoint: MOCK_VERIFY_ENDPOINT, + }, + ), + ).rejects.toThrow( + "Expected vc.id to be a string, found [undefined] of type [undefined] on", ); }); @@ -215,7 +281,7 @@ describe("isValidVc", () => { verificationEndpoint: MOCK_VERIFY_ENDPOINT, }), ).rejects.toThrow( - "The request to [https://example.com/someVc] returned an unexpected response: undefined", + "Parsing the Verifiable Credential [https://example.com/someVc] as JSON failed: SyntaxError: Unexpected token 'N', \"Not a valid JSON.\" is not valid JSON", ); }); @@ -233,7 +299,7 @@ describe("isValidVc", () => { verificationEndpoint: MOCK_VERIFY_ENDPOINT, }), ).rejects.toThrow( - "The request to [https://example.com/someVc] returned an unexpected response:", + "The value received from [https://example.com/someVc] is not a Verifiable Credential", ); }); @@ -243,7 +309,6 @@ describe("isValidVc", () => { status: 200, }), ); - mocked(isVerifiableCredential).mockReturnValueOnce(true); await isValidVc(MOCK_VC, { fetch: mockedFetch as (typeof UniversalFetch)["fetch"], @@ -261,7 +326,7 @@ describe("isValidVc", () => { legacy: {}, specCompliant: {}, }); - mocked(isVerifiableCredential).mockReturnValueOnce(true); + const mockedFetch = jest .fn(global.fetch) // First, the VC is fetched @@ -291,7 +356,6 @@ describe("isValidVc", () => { statusText: "Bad request", }), ); - mocked(isVerifiableCredential).mockReturnValueOnce(true); await expect( isValidVc(MOCK_VC, { @@ -305,7 +369,6 @@ describe("isValidVc", () => { const mockedFetch = jest .fn(global.fetch) .mockResolvedValueOnce(new Response("Not a valid JSON")); - mocked(isVerifiableCredential).mockReturnValueOnce(true); await expect( isValidVc(MOCK_VC, { @@ -321,7 +384,6 @@ describe("isValidVc", () => { status: 200, }), ); - mocked(isVerifiableCredential).mockReturnValueOnce(true); await expect( isValidVc(MOCK_VC, { @@ -345,13 +407,15 @@ describe("isValidVerifiable Presentation", () => { const mockedFetch = jest.requireMock( "@inrupt/universal-fetch", ) as jest.Mocked; - mocked(isVerifiablePresentation).mockReturnValueOnce(true); + // mocked(isVerifi).mockReturnValueOnce(true); mockedFetch.fetch.mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), ); - await isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP); + await expect( + isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP), + ).resolves.toMatchObject({ errors: [] }); expect(mockedFetch.fetch).toHaveBeenCalled(); }); @@ -464,23 +528,141 @@ describe("isValidVerifiable Presentation", () => { ); }); - it("throws if passed VP is not a verifiable presentation", async () => { + it("throws if passed VP is not a verifiable presentation [because the VC has no id]", async () => { const mockedFetch = jest.fn(global.fetch); mocked(isVerifiablePresentation).mockReturnValueOnce(false); + const MOCK_VP_NO_ID = { + ...MOCK_VP, + verifiableCredential: [ + { + ...MOCK_VC, + id: undefined as unknown as string, + }, + ], + }; + await expect( - isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP, { + isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP_NO_ID, { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, }), ).rejects.toThrow( - `The request to [${MOCK_VP}] returned an unexpected response: ${JSON.stringify( - MOCK_VP, + `Expected vc.id to be a string, found [undefined] of type [undefined] on ${JSON.stringify( + { + ...MOCK_VC, + id: undefined as unknown as string, + }, null, " ", )}`, ); }); + it("throws if passed VP is not a verifiable presentation [because it is the wrong type]", async () => { + const mockedFetch = jest.fn(global.fetch); + mocked(isVerifiablePresentation).mockReturnValueOnce(false); + + const MOCK_VP_NO_ID = Object.assign( + await jsonLdStringToStore(JSON.stringify(MOCK_VP)), + { verifiableCredential: [] }, + ); + for (const quad of MOCK_VP_NO_ID.match(null, rdf.type, null, null)) { + MOCK_VP_NO_ID.delete(quad); + } + + await expect( + isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP_NO_ID, { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + }), + ).rejects.toThrow("Expected exactly one Verifiable Presentation. Found 0."); + }); + + it("throws if passed VP is not a verifiable presentation [because it has too many holders]", async () => { + const mockedFetch = jest.fn(global.fetch); + mocked(isVerifiablePresentation).mockReturnValueOnce(false); + + const MOCK_VP_EXTRA_HOLDER = Object.assign( + await jsonLdStringToStore(JSON.stringify(MOCK_VP)), + { verifiableCredential: [] }, + ); + MOCK_VP_EXTRA_HOLDER.add( + quad( + getVpSubject(MOCK_VP_EXTRA_HOLDER), + cred.holder, + namedNode("http://example.org/another/holder"), + ), + ); + + expect(() => + getHolder(MOCK_VP_EXTRA_HOLDER, getVpSubject(MOCK_VP_EXTRA_HOLDER)), + ).toThrow("Could not find a valid holder"); + await expect( + isValidVerifiablePresentation( + MOCK_VERIFY_ENDPOINT, + MOCK_VP_EXTRA_HOLDER, + { + fetch: + mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + }, + ), + ).rejects.toThrow( + "The request to [[object Object]] returned an unexpected response", + ); + }); + + it("throws if passed VP is not a verifiable presentation [included VC is missing type]", async () => { + const mockedFetch = jest.fn(global.fetch); + mocked(isVerifiablePresentation).mockReturnValueOnce(false); + + const VC_STORE = await jsonLdStringToStore(JSON.stringify(MOCK_VC)); + VC_STORE.delete( + quad(namedNode(MOCK_VC.id), rdf.type, cred.VerifiableCredential), + ); + + const MOCK_VP_EXTRA_HOLDER = Object.assign( + await jsonLdStringToStore(JSON.stringify(MOCK_VP)), + { verifiableCredential: [Object.assign(VC_STORE, { id: MOCK_VC.id })] }, + ); + await expect( + isValidVerifiablePresentation( + MOCK_VERIFY_ENDPOINT, + MOCK_VP_EXTRA_HOLDER, + { + fetch: + mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + }, + ), + ).rejects.toThrow( + "The request to [[object Object]] returned an unexpected response", + ); + await expect( + isValidVc(Object.assign(VC_STORE, { id: MOCK_VC.id }), { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + }), + ).rejects.toThrow( + "The request to [[object Object]] returned an unexpected response", + ); + }); + + it("throws if passed VP is not a verifiable presentation [because it has an invalid subject]", async () => { + const mockedFetch = jest.fn(global.fetch); + mocked(isVerifiablePresentation).mockReturnValueOnce(false); + + const MOCK_VP_NO_TYPE = new Store([ + // @ts-expect-error literals should not be subjects of triples + quad(literal("vp subpect"), rdf.type, cred.VerifiablePresentation), + ]); + + await expect( + // @ts-expect-error the vp is also missing the verifiableCredential property + isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP_NO_TYPE, { + fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + }), + ).rejects.toThrow( + "Expected VP subject to be NamedNode or BlankNode. Instead found [vp subpect] with termType [Literal]", + ); + }); + it("throws if response is not valid JSON", async () => { const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( new Response(`some non-JSON response`, { @@ -521,7 +703,6 @@ describe("isValidVerifiable Presentation", () => { }), ); mocked(isVerifiablePresentation).mockReturnValueOnce(true); - await expect( isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP, { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, diff --git a/src/verify/verify.ts b/src/verify/verify.ts index c1a1bb66..6e866013 100644 --- a/src/verify/verify.ts +++ b/src/verify/verify.ts @@ -26,25 +26,47 @@ import type { UrlString } from "@inrupt/solid-client"; import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; +import type { DatasetCore } from "@rdfjs/types"; +import { DataFactory } from "n3"; import type { + DatasetWithId, VerifiableCredentialBase, VerifiablePresentation, } from "../common/common"; import { getVerifiableCredential, getVerifiableCredentialApiConfiguration, - isVerifiableCredential, - isVerifiablePresentation, + hasId, + verifiableCredentialToDataset, } from "../common/common"; +import { getId, getIssuer } from "../common/getters"; +import isRdfjsVerifiableCredential from "../common/isRdfjsVerifiableCredential"; +import isRdfjsVerifiablePresentation, { + getHolder, + getVpSubject, +} from "../common/isRdfjsVerifiablePresentation"; +import type { + MinimalPresentation, + ParsedVerifiablePresentation, +} from "../lookup/query"; import type { ParseOptions } from "../parser/jsonld"; +const { namedNode } = DataFactory; + async function dereferenceVc( - vc: VerifiableCredentialBase | URL | UrlString, - options?: ParseOptions, -): Promise { + vc: VerifiableCredentialBase | DatasetWithId | URL | UrlString, + options?: ParseOptions & { + requireId?: boolean; + }, +): Promise { // This test passes for both URL and UrlString if (!vc.toString().startsWith("http")) { - return vc as VerifiableCredentialBase; + if (typeof (vc as DatasetCore).match === "function") { + return vc as DatasetCore; + } + return verifiableCredentialToDataset(vc as VerifiableCredentialBase, { + requireId: true, + }); } return getVerifiableCredential(vc.toString(), options); } @@ -68,7 +90,7 @@ async function dereferenceVc( * @since 0.3.0 */ export async function isValidVc( - vc: VerifiableCredentialBase | URL | UrlString, + vc: VerifiableCredentialBase | DatasetWithId | URL | UrlString, options?: Partial<{ fetch?: typeof fetch; verificationEndpoint?: UrlString; @@ -79,7 +101,10 @@ export async function isValidVc( const vcObject = await dereferenceVc(vc, options); - if (!isVerifiableCredential(vcObject)) { + if ( + !hasId(vcObject) || + !isRdfjsVerifiableCredential(vcObject, namedNode(getId(vcObject))) + ) { throw new Error( `The request to [${vc}] returned an unexpected response: ${JSON.stringify( vcObject, @@ -92,12 +117,14 @@ export async function isValidVc( // Discover the consent endpoint from the resource part of the Access Grant. const verifierEndpoint = options?.verificationEndpoint ?? - (await getVerifiableCredentialApiConfiguration(vcObject.issuer)) + (await getVerifiableCredentialApiConfiguration(getIssuer(vcObject))) .verifierService; if (verifierEndpoint === undefined) { throw new Error( - `The VC service provider ${vcObject.issuer} does not advertize for a verifier service in its .well-known/vc-configuration document`, + `The VC service provider ${getIssuer( + vcObject, + )} does not advertize for a verifier service in its .well-known/vc-configuration document`, ); } @@ -129,6 +156,25 @@ export async function isValidVc( } } +async function asDataset( + data: DatasetCore | VerifiablePresentation, + requireId: true, +): Promise; +async function asDataset( + data: DatasetCore | VerifiablePresentation, + requireId: boolean, +): Promise; +async function asDataset( + data: DatasetCore | VerifiablePresentation, + requireId: boolean, +): Promise { + return typeof (data as DatasetCore).match === "function" + ? (data as DatasetCore) + : verifiableCredentialToDataset(data as VerifiablePresentation, { + requireId, + }); +} + /** * Verify that a VP is valid and content has not ben tampered with. * @@ -146,7 +192,10 @@ export async function isValidVc( */ export async function isValidVerifiablePresentation( verificationEndpoint: string | null, - verifiablePresentation: VerifiablePresentation, + verifiablePresentation: + | VerifiablePresentation + | MinimalPresentation + | ParsedVerifiablePresentation, options: Partial<{ fetch: typeof fetch; domain: string; @@ -154,28 +203,51 @@ export async function isValidVerifiablePresentation( }> = {}, ): Promise<{ checks: string[]; warnings: string[]; errors: string[] }> { const fetcher = options.fetch ?? fallbackFetch; + const dataset = await asDataset(verifiablePresentation, false); + const subject = getVpSubject(dataset); - if (!isVerifiablePresentation(verifiablePresentation)) { + if (!isRdfjsVerifiablePresentation(dataset, subject)) { throw new Error( - `The request to [${verifiablePresentation}] returned an unexpected response: ${JSON.stringify( - verifiablePresentation, + `The request to [${dataset}] returned an unexpected response: ${JSON.stringify( + dataset, null, " ", )}`, ); } + if (verifiablePresentation.verifiableCredential) { + const datasets = await Promise.all( + verifiablePresentation.verifiableCredential.map(async (vc) => { + const vcDataset = await asDataset(vc, true); + return isRdfjsVerifiableCredential( + vcDataset, + namedNode(getId(vcDataset)), + ); + }), + ); + if (datasets.some((vc) => vc === false)) { + throw new Error( + `The request to [${dataset}] returned an unexpected response: ${JSON.stringify( + dataset, + null, + " ", + )}`, + ); + } + } + const verifierEndpoint = verificationEndpoint ?? - ( - await getVerifiableCredentialApiConfiguration( - verifiablePresentation.holder as string, - ) - ).verifierService; + (await getVerifiableCredentialApiConfiguration(getHolder(dataset, subject))) + .verifierService; if (verifierEndpoint === undefined) { throw new Error( - `The VC service provider ${verifiablePresentation.holder} does not advertize for a verifier service in its .well-known/vc-configuration document`, + `The VC service provider ${getHolder( + dataset, + subject, + )} does not advertize for a verifier service in its .well-known/vc-configuration document`, ); } @@ -185,7 +257,7 @@ export async function isValidVerifiablePresentation( }, method: "POST", body: JSON.stringify({ - verifiablePresentation, + verifiablePresentation: dataset, options: { domain: options.domain, challenge: options.challenge, From 707cb3405ecd8973eb19b7315cddf636321593a2 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:05:20 +0000 Subject: [PATCH 70/79] chore: fix errors messaging --- src/verify/verify.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/verify/verify.test.ts b/src/verify/verify.test.ts index 5d221fc3..f9ea8fd9 100644 --- a/src/verify/verify.test.ts +++ b/src/verify/verify.test.ts @@ -281,7 +281,7 @@ describe("isValidVc", () => { verificationEndpoint: MOCK_VERIFY_ENDPOINT, }), ).rejects.toThrow( - "Parsing the Verifiable Credential [https://example.com/someVc] as JSON failed: SyntaxError: Unexpected token 'N', \"Not a valid JSON.\" is not valid JSON", + "Parsing the Verifiable Credential [https://example.com/someVc] as JSON failed:", ); }); From dedcc1b16e31ab2fc5d50ee303e1b5753b1c188c Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:17:52 +0000 Subject: [PATCH 71/79] chore: fix docs:build errors --- src/lookup/query.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lookup/query.ts b/src/lookup/query.ts index fe4eb571..a2b0806c 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -76,6 +76,9 @@ export type VerifiablePresentationRequest = { domain?: string; }; +/** + * @hidden + */ export interface ParsedVerifiablePresentation extends VerifiablePresentation, DatasetCore { From 0d375a62208192b074d6c7f600679d4a75a582c1 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:37:02 +0000 Subject: [PATCH 72/79] chore: mark internal_getVerifiableCredentialFromResponse as hidden --- src/common/common.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/common/common.ts b/src/common/common.ts index a3f8828a..c74d3e7a 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -559,6 +559,9 @@ export function hasId(vc: unknown): vc is { id: string } { ); } +/** + * @hidden + */ // eslint-disable-next-line camelcase export async function internal_getVerifiableCredentialFromResponse( vcUrl: UrlString | undefined, From c135d414e9cb5c2b76b6ff0b41f19f8c6f724ff6 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:47:20 +0000 Subject: [PATCH 73/79] chore: remove variable shadowing --- src/verify/verify.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/verify/verify.test.ts b/src/verify/verify.test.ts index f9ea8fd9..dac4f7db 100644 --- a/src/verify/verify.test.ts +++ b/src/verify/verify.test.ts @@ -566,8 +566,13 @@ describe("isValidVerifiable Presentation", () => { await jsonLdStringToStore(JSON.stringify(MOCK_VP)), { verifiableCredential: [] }, ); - for (const quad of MOCK_VP_NO_ID.match(null, rdf.type, null, null)) { - MOCK_VP_NO_ID.delete(quad); + for (const quadToDelete of MOCK_VP_NO_ID.match( + null, + rdf.type, + null, + null, + )) { + MOCK_VP_NO_ID.delete(quadToDelete); } await expect( From 856343609ae1a50af04efd6615aa403c5f40d855 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:47:44 +0000 Subject: [PATCH 74/79] chore: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f77f9462..414959c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The following changes have been implemented but not released yet: ## Unreleased +## Breaking Changes + +- Parsing Verifiable Credentials. This allows the Verifiable Credential to be read using the RDF/JS DatasetCore API. This is a breaking change because the `VerifiableCredential` type now is also of type `DatasetCore`. Importantly, this dataset is not preserved when converting to verifiableCredentials a string and back doing `JSON.parse(JSON.stringify(verifiableCredential))`. We reccomend that developers set `returnLegacyJsonld` to `false` in functions such as `getVerifiableCredential` in order to avoid returning deprecated object properties. Instead developers should make use of the exported `getter` functions to get these attributes. + ## [0.7.4](https://github.com/inrupt/solid-client-vc-js/releases/tag/v0.7.4) - 2023-11-17 ### Internal Changes From 23ae3a2775bfecac1c60df3d17ccea591f28f41c Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:51:20 +0000 Subject: [PATCH 75/79] chore: fix breaking changes header --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 414959c5..67d822f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The following changes have been implemented but not released yet: ## Unreleased -## Breaking Changes +### Breaking Changes - Parsing Verifiable Credentials. This allows the Verifiable Credential to be read using the RDF/JS DatasetCore API. This is a breaking change because the `VerifiableCredential` type now is also of type `DatasetCore`. Importantly, this dataset is not preserved when converting to verifiableCredentials a string and back doing `JSON.parse(JSON.stringify(verifiableCredential))`. We reccomend that developers set `returnLegacyJsonld` to `false` in functions such as `getVerifiableCredential` in order to avoid returning deprecated object properties. Instead developers should make use of the exported `getter` functions to get these attributes. From 56fa718e5fc5ffac317c5e0c9d2aea8f207605a1 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:11:42 +0000 Subject: [PATCH 76/79] chore: fix build errors --- e2e/node/e2e.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/e2e/node/e2e.test.ts b/e2e/node/e2e.test.ts index a4241300..a688865c 100644 --- a/e2e/node/e2e.test.ts +++ b/e2e/node/e2e.test.ts @@ -52,8 +52,6 @@ const validCredentialClaims = { "https://schema.inrupt.com/credentials/v1.jsonld", ], type: ["SolidAccessRequest"], - "http://example.org/my/filtering/property": - "http://example.org/my/filtering/object", expirationDate: new Date(Date.now() + 15 * 60 * 1000).toISOString(), }; const validSubjectClaims = (options?: { @@ -349,7 +347,7 @@ describe("End-to-end verifiable credentials tests for environment", () => { }, ), ]); - + expect(credential1.credentialSubject.id).toBe(vcSubject); expect(getCredentialSubject(credential1).value).toBe(vcSubject); expect(credential2.credentialSubject.id).toBe(vcSubject); @@ -397,7 +395,6 @@ describe("End-to-end verifiable credentials tests for environment", () => { } as unknown as VerifiablePresentationRequest, { fetch: session.fetch, - includeExpiredVc: false, }, ), query( From 4b6051bd4ccdfbcb6dc7279d1a4592d148edf4fc Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:53:07 +0000 Subject: [PATCH 77/79] breaking: remove use of universal fetch --- CHANGELOG.md | 2 + jest.setup.ts | 3 + package-lock.json | 1 - package.json | 1 - src/common/common.test.ts | 191 ++++++++++++++++--------------- src/common/common.ts | 3 +- src/common/configuration.test.ts | 2 - src/issue/issue.test.ts | 106 +++++++---------- src/issue/issue.ts | 22 ++-- src/lookup/derive.test.ts | 85 ++++++-------- src/lookup/derive.ts | 11 +- src/lookup/query.test.ts | 138 ++++++++++------------ src/lookup/query.ts | 11 +- src/parser/jsonld.test.ts | 13 +-- src/parser/jsonld.ts | 7 +- src/revoke/revoke.test.ts | 51 +++------ src/revoke/revoke.ts | 3 +- src/verify/verify.test.ts | 141 ++++++++++------------- src/verify/verify.ts | 5 +- 19 files changed, 344 insertions(+), 452 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67d822f5..21cd8603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The following changes have been implemented but not released yet: ### Breaking Changes - Parsing Verifiable Credentials. This allows the Verifiable Credential to be read using the RDF/JS DatasetCore API. This is a breaking change because the `VerifiableCredential` type now is also of type `DatasetCore`. Importantly, this dataset is not preserved when converting to verifiableCredentials a string and back doing `JSON.parse(JSON.stringify(verifiableCredential))`. We reccomend that developers set `returnLegacyJsonld` to `false` in functions such as `getVerifiableCredential` in order to avoid returning deprecated object properties. Instead developers should make use of the exported `getter` functions to get these attributes. +- Use the global `fetch` function instead of `@inrupt/universal-fetch`. This means this library now only works + with Node 18 and higher. ## [0.7.4](https://github.com/inrupt/solid-client-vc-js/releases/tag/v0.7.4) - 2023-11-17 diff --git a/jest.setup.ts b/jest.setup.ts index 39514e1d..51c79d5c 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -20,3 +20,6 @@ // import "@inrupt/jest-jsdom-polyfills"; +globalThis.fetch = () => { + throw new Error("Fetch should not be called in tests without being mocked"); +} diff --git a/package-lock.json b/package-lock.json index bc02c68c..b08dc78e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "@inrupt/solid-client": "^1.25.2", - "@inrupt/universal-fetch": "^1.0.1", "event-emitter-promisify": "^1.1.0", "jsonld-context-parser": "^2.4.0", "jsonld-streaming-parser": "^3.3.0", diff --git a/package.json b/package.json index 4c4ab015..90bbff23 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,6 @@ }, "dependencies": { "@inrupt/solid-client": "^1.25.2", - "@inrupt/universal-fetch": "^1.0.1", "event-emitter-promisify": "^1.1.0", "jsonld-context-parser": "^2.4.0", "jsonld-streaming-parser": "^3.3.0", diff --git a/src/common/common.test.ts b/src/common/common.test.ts index 735b7f94..ef902d15 100644 --- a/src/common/common.test.ts +++ b/src/common/common.test.ts @@ -18,8 +18,6 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type * as UniversalFetch from "@inrupt/universal-fetch"; -import { Response, fetch as uniFetch } from "@inrupt/universal-fetch"; import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import type { IJsonLdContext } from "jsonld-context-parser"; import { DataFactory, Store } from "n3"; @@ -49,16 +47,8 @@ import isRdfjsVerifiablePresentation from "./isRdfjsVerifiablePresentation"; const { namedNode, quad, blankNode } = DataFactory; -jest.mock("@inrupt/universal-fetch", () => { - const fetchModule = jest.requireActual( - "@inrupt/universal-fetch", - ) as typeof UniversalFetch; - return { - ...fetchModule, - fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(() => { - throw new Error("Fetch should not be called"); - }), - }; +const spiedFetch = jest.spyOn(globalThis, "fetch").mockImplementation(() => { + throw new Error("Unexpected fetch call"); }); describe("normalizeVc", () => { @@ -466,13 +456,10 @@ describe("concatenateContexts", () => { describe("getVerifiableCredential", () => { describe("defaults to an unauthenticated fetch", () => { - let mockedFetchModule: jest.Mocked; + let mockedFetch: jest.Spied; beforeEach(() => { - mockedFetchModule = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; - mockedFetchModule.fetch.mockResolvedValueOnce( + mockedFetch = jest.spyOn(globalThis, "fetch").mockResolvedValueOnce( new Response(JSON.stringify(mockDefaultCredential()), { headers: new Headers([["content-type", "application/json"]]), }), @@ -493,7 +480,7 @@ describe("getVerifiableCredential", () => { await getVerifiableCredential( "https://example.org/ns/someCredentialInstance", ); - expect(mockedFetchModule.fetch).toHaveBeenCalledWith( + expect(mockedFetch).toHaveBeenCalledWith( "https://example.org/ns/someCredentialInstance", ); }); @@ -504,23 +491,21 @@ describe("getVerifiableCredential", () => { returnLegacyJsonld: false, }, ); - expect(mockedFetchModule.fetch).toHaveBeenCalledWith( + expect(mockedFetch).toHaveBeenCalledWith( "https://example.org/ns/someCredentialInstance", ); }); }); describe("uses the provided fetch if any", () => { - let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + let mockedFetch: jest.MockedFunction; beforeEach(() => { - mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultCredential()), { - headers: new Headers([["content-type", "application/json"]]), - }), - ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + mockedFetch = jest.fn().mockResolvedValueOnce( + new Response(JSON.stringify(mockDefaultCredential()), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); }); it.each([[true], [false]])( @@ -529,7 +514,7 @@ describe("getVerifiableCredential", () => { await getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ); @@ -541,17 +526,15 @@ describe("getVerifiableCredential", () => { }); describe("throws if the VC ID cannot be dereferenced", () => { - let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + let mockedFetch: jest.MockedFunction; beforeEach(() => { - mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(undefined, { - status: 401, - statusText: "Unauthenticated", - }), - ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + mockedFetch = jest.fn().mockResolvedValueOnce( + new Response(undefined, { + status: 401, + statusText: "Unauthenticated", + }), + ); }); it.each([[true], [false]])( @@ -561,7 +544,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -573,14 +556,12 @@ describe("getVerifiableCredential", () => { }); describe("throws if the dereferenced data is invalid JSON", () => { - let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + let mockedFetch: jest.MockedFunction; beforeEach(() => { mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce(new Response("Not JSON")) as jest.MockedFunction< - (typeof UniversalFetch)["fetch"] - >; + .fn() + .mockResolvedValueOnce(new Response("Not JSON")); }); it.each([[true], [false]])( @@ -590,7 +571,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -602,14 +583,14 @@ describe("getVerifiableCredential", () => { }); describe("throws if the dereferenced data is not a VC", () => { - let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + let mockedFetch: jest.MockedFunction; beforeEach(() => { mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() + .fn() .mockResolvedValueOnce( new Response(JSON.stringify({ something: "but not a VC" })), - ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + ); }); it.each([[true], [false]])( @@ -619,7 +600,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -636,14 +617,14 @@ describe("getVerifiableCredential", () => { // see https://github.com/inrupt/solid-client-vc-js/pull/849#discussion_r1414124524 it.skip("throws if the dereferenced data has an unsupported content type", async () => { const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() + .fn() .mockResolvedValueOnce( new Response(JSON.stringify(mockDefaultCredential())), ); await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }), ).rejects.toThrow(/unsupported Content-Type/); }); @@ -651,7 +632,7 @@ describe("getVerifiableCredential", () => { it.each([[true], [false]])( "throws if the dereferenced data is emptyreturnLegacyJsonld: [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify({}), { headers: new Headers([["content-type", "application/json"]]), @@ -662,7 +643,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -677,7 +658,7 @@ describe("getVerifiableCredential", () => { it.each([[true], [false]])( "throws if the vc is a blank node: [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response( JSON.stringify({ @@ -692,7 +673,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }), ).rejects.toThrow( @@ -706,7 +687,7 @@ describe("getVerifiableCredential", () => { it.each([[true], [false]])( "throws if the vc has a type that is a literal: [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response( JSON.stringify({ @@ -728,7 +709,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }), ).rejects.toThrow( @@ -742,7 +723,7 @@ describe("getVerifiableCredential", () => { it.each([[true], [false]])( "throws if the dereferenced data has 2 vcs: [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response( JSON.stringify([ @@ -757,7 +738,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }), ).rejects.toThrow( @@ -771,7 +752,7 @@ describe("getVerifiableCredential", () => { it.each([[true], [false]])( "throws if the dereferenced data has 2 proofs: [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify(mockDefaultCredential2Proofs()), { headers: new Headers([["content-type", "application/json"]]), @@ -780,7 +761,7 @@ describe("getVerifiableCredential", () => { await expect( getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }), ).rejects.toThrow( @@ -795,7 +776,7 @@ describe("getVerifiableCredential", () => { const mocked = mockDefaultCredential(); mocked.issuanceDate = "http://example.org/not/a/date"; - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), @@ -806,7 +787,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -825,7 +806,7 @@ describe("getVerifiableCredential", () => { mocked["https://www.w3.org/2018/credentials#issuanceDate"] = "http://example.org/not/a/date"; - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), @@ -836,7 +817,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -856,7 +837,7 @@ describe("getVerifiableCredential", () => { "@id": "http://example.org/not/a/date", }; - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), @@ -867,7 +848,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -884,7 +865,7 @@ describe("getVerifiableCredential", () => { // @ts-expect-error issuer is of type string on the VC type mocked.issuer = { "@value": "my string" }; - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), @@ -895,7 +876,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -918,7 +899,7 @@ describe("getVerifiableCredential", () => { }, }; - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), @@ -928,13 +909,13 @@ describe("getVerifiableCredential", () => { const vc = await getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }, ); const vcNoLegacy = await getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld: false, }, ); @@ -969,7 +950,7 @@ describe("getVerifiableCredential", () => { }, }; - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), @@ -979,14 +960,14 @@ describe("getVerifiableCredential", () => { const vc = await getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }, ); const vcNoLegacy = await getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld: false, }, ); @@ -1009,7 +990,7 @@ describe("getVerifiableCredential", () => { // @ts-expect-error proofValue is a string not string[] in VC type mocked.proof.proofValue = [mocked.proof.proofValue, "abc"]; - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify(mocked), { headers: new Headers([["content-type", "application/json"]]), @@ -1020,7 +1001,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -1031,7 +1012,7 @@ describe("getVerifiableCredential", () => { ); it("returns the fetched VC and the redirect URL", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify(mockDefaultCredential()), { headers: new Headers([["content-type", "application/json"]]), @@ -1039,7 +1020,7 @@ describe("getVerifiableCredential", () => { ); const vc = await getVerifiableCredential("https://some.vc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }); const res = await jsonLdStringToStore( @@ -1087,16 +1068,14 @@ describe("getVerifiableCredential", () => { "http://example.org/my/sample/context", ], }; - let mockedFetch: jest.Mock<(typeof UniversalFetch)["fetch"]>; + let mockedFetch: jest.Mock; beforeEach(() => { - mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify(mockCredential), { - headers: new Headers([["content-type", "application/json"]]), - }), - ); + mockedFetch = jest.fn().mockResolvedValueOnce( + new Response(JSON.stringify(mockCredential), { + headers: new Headers([["content-type", "application/json"]]), + }), + ); }); it.each([[true], [false]])( @@ -1106,7 +1085,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -1119,7 +1098,7 @@ describe("getVerifiableCredential", () => { it.each([[true], [false]])( "resolves if allowContextFetching is enabled and the context can be fetched: [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - (uniFetch as jest.Mock).mockResolvedValueOnce( + (fetch as jest.Mock).mockResolvedValueOnce( new Response( JSON.stringify({ "@context": {}, @@ -1134,7 +1113,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, allowContextFetching: true, returnLegacyJsonld, }, @@ -1150,7 +1129,7 @@ describe("getVerifiableCredential", () => { ); it("can apply normalization of the response before parsing and returning it", async () => { - (uniFetch as jest.Mock).mockResolvedValueOnce( + (fetch as jest.Mock).mockResolvedValueOnce( new Response( JSON.stringify({ "@context": {}, @@ -1164,7 +1143,7 @@ describe("getVerifiableCredential", () => { const res = await getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, allowContextFetching: true, normalize: (data) => ({ ...data, @@ -1193,7 +1172,18 @@ describe("getVerifiableCredential", () => { }); it("resolves if allowContextFetching is enabled and the context can be fetched [returnLegacyJsonld: false]", async () => { - (uniFetch as jest.Mock).mockResolvedValueOnce( + spiedFetch.mockResolvedValue( + new Response( + JSON.stringify({ + "@context": {}, + }), + { + headers: new Headers([["content-type", "application/ld+json"]]), + }, + ), + ); + + mockedFetch.mockResolvedValueOnce( new Response( JSON.stringify({ "@context": {}, @@ -1208,7 +1198,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, allowContextFetching: true, returnLegacyJsonld: false, }, @@ -1216,6 +1206,19 @@ describe("getVerifiableCredential", () => { ).resolves.toMatchObject({ id: "https://example.org/ns/someCredentialInstance", }); + + // Should use the authenticated fetch to get the credential + expect(mockedFetch).toHaveBeenCalledTimes(1); + expect(mockedFetch).toHaveBeenCalledWith( + "https://example.org/ns/someCredentialInstance", + ); + + // Should use the unauthenticated fetch to fetch contexts + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith( + "http://example.org/my/sample/context", + expect.anything(), + ); }); it.each([[true], [false]])( @@ -1225,7 +1228,7 @@ describe("getVerifiableCredential", () => { getVerifiableCredential( "https://example.org/ns/someCredentialInstance", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, allowContextFetching: true, returnLegacyJsonld, contexts: { diff --git a/src/common/common.ts b/src/common/common.ts index c74d3e7a..d0635553 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -30,7 +30,6 @@ import { getSolidDataset, getThingAll, } from "@inrupt/solid-client"; -import { fetch as uniFetch } from "@inrupt/universal-fetch"; import type { DatasetCore, Quad } from "@rdfjs/types"; import { DataFactory } from "n3"; import type { ParseOptions } from "../parser/jsonld"; @@ -740,7 +739,7 @@ export async function getVerifiableCredential( normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise { - const authFetch = options?.fetch ?? uniFetch; + const authFetch = options?.fetch ?? fetch; const response = await authFetch(vcUrl); if (!response.ok) { diff --git a/src/common/configuration.test.ts b/src/common/configuration.test.ts index f50f607d..7d25c943 100644 --- a/src/common/configuration.test.ts +++ b/src/common/configuration.test.ts @@ -29,8 +29,6 @@ import { import type * as SolidClient from "@inrupt/solid-client"; import { getVerifiableCredentialApiConfiguration } from "./common"; -jest.mock("@inrupt/universal-fetch"); - jest.mock("@inrupt/solid-client", () => { const solidClientModule = jest.requireActual( "@inrupt/solid-client", diff --git a/src/issue/issue.test.ts b/src/issue/issue.test.ts index 5a2f43a6..5b7c7c40 100644 --- a/src/issue/issue.test.ts +++ b/src/issue/issue.test.ts @@ -20,34 +20,22 @@ // import { jest, describe, it, expect } from "@jest/globals"; -import { Response } from "@inrupt/universal-fetch"; -import type * as UniversalFetch from "@inrupt/universal-fetch"; import { defaultContext, defaultCredentialTypes } from "../common/common"; import { mockDefaultCredential } from "../common/common.mock"; import defaultIssueVerifiableCredential, { issueVerifiableCredential, } from "./issue"; -jest.mock("@inrupt/universal-fetch", () => { - const fetchModule = jest.requireActual( - "@inrupt/universal-fetch", - ) as jest.Mocked; - return { - ...fetchModule, - fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(), - }; -}); - describe("issueVerifiableCredential", () => { it("uses the provided fetch if any", async () => { - const mockedFetch = jest.fn() as (typeof UniversalFetch)["fetch"]; + const mockedFetch = jest.fn() as typeof fetch; try { await issueVerifiableCredential( "https://some.endpoint", { "@context": ["https://some.context"] }, { "@context": ["https://some.context"] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }, ); // eslint-disable-next-line no-empty @@ -56,9 +44,9 @@ describe("issueVerifiableCredential", () => { }); it("defaults to an unauthenticated fetch if no fetch is provided", async () => { - const mockedFetchModule = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; + const spiedFetch = jest + .spyOn(globalThis, "fetch") + .mockResolvedValueOnce(new Response()); try { await issueVerifiableCredential( "https://some.endpoint", @@ -67,24 +55,22 @@ describe("issueVerifiableCredential", () => { ); // eslint-disable-next-line no-empty } catch (_e) {} - expect(mockedFetchModule.fetch).toHaveBeenCalled(); + expect(spiedFetch).toHaveBeenCalled(); }); it("throws if the issuer returns an error", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(undefined, { - status: 400, - statusText: "Bad request", - }), - ); + const mockedFetch = jest.fn().mockResolvedValueOnce( + new Response(undefined, { + status: 400, + statusText: "Bad request", + }), + ); await expect( issueVerifiableCredential( "https://some.endpoint", { "@context": ["https://some.context"] }, { "@context": ["https://some.context"] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ), ).rejects.toThrow( /https:\/\/some\.endpoint.*could not successfully issue a VC.*400.*Bad request/, @@ -92,19 +78,17 @@ describe("issueVerifiableCredential", () => { }); it("throws if the returned value does not conform to the shape we expect", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify({ someField: "Not a credential" }), { - status: 201, - }), - ); + const mockedFetch = jest.fn().mockResolvedValueOnce( + new Response(JSON.stringify({ someField: "Not a credential" }), { + status: 201, + }), + ); await expect( issueVerifiableCredential( "https://some.endpoint", { "@context": ["https://some.context"] }, { "@context": ["https://some.context"] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ), ).rejects.toThrow( "Parsing the Verifiable Credential [undefined] as JSON failed: Error: Cannot establish id of verifiable credential", @@ -112,20 +96,18 @@ describe("issueVerifiableCredential", () => { }); it("returns the VC issued by the target issuer", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultCredential()), { - status: 201, - headers: new Headers([["content-type", "application/ld+json"]]), - }), - ); + const mockedFetch = jest.fn().mockResolvedValueOnce( + new Response(JSON.stringify(mockDefaultCredential()), { + status: 201, + headers: new Headers([["content-type", "application/ld+json"]]), + }), + ); const vc = await issueVerifiableCredential( "https://some.endpoint", { "@context": ["https://some.context"] }, { "@context": ["https://some.context"] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ); expect(vc).toMatchObject({ ...mockDefaultCredential(), size: 13 }); @@ -134,13 +116,13 @@ describe("issueVerifiableCredential", () => { }); it("sends a request to the specified issuer", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>(); + const mockedFetch = jest.fn(); try { await issueVerifiableCredential( "https://some.endpoint", { "@context": ["https://some.context"] }, { "@context": ["https://some.context"] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ); // eslint-disable-next-line no-empty } catch (_e) {} @@ -151,13 +133,13 @@ describe("issueVerifiableCredential", () => { }); it("sends a POST request with the appropriate headers", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>(); + const mockedFetch = jest.fn(); try { await issueVerifiableCredential( "https://some.endpoint", { "@context": ["https://some.context"] }, { "@context": ["https://some.context"] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ); // eslint-disable-next-line no-empty } catch (_e) {} @@ -173,13 +155,13 @@ describe("issueVerifiableCredential", () => { }); it("includes the subject and subject claims in the request body", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>(); + const mockedFetch = jest.fn(); try { await issueVerifiableCredential( "https://some.endpoint", { "@context": ["https://some-subject.context"], aClaim: "a value" }, undefined, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ); // eslint-disable-next-line no-empty } catch (_e) {} @@ -206,7 +188,7 @@ describe("issueVerifiableCredential", () => { "https://some.endpoint", { "@context": ["https://some-subject.context"] }, { "@context": ["https://some-credential.context"], aClaim: "a value" }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ); // eslint-disable-next-line no-empty } catch (_e) {} @@ -230,13 +212,13 @@ describe("issueVerifiableCredential", () => { }); it("includes the credential type in the request body", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>(); + const mockedFetch = jest.fn(); try { await issueVerifiableCredential( "https://some.endpoint", { "@context": ["https://some-subject.context"] }, { "@context": ["https://some-credential.context"], type: "some-type" }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ); // eslint-disable-next-line no-empty } catch (_e) {} @@ -259,7 +241,7 @@ describe("issueVerifiableCredential", () => { }); it("supports credentials with multiple types", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>(); + const mockedFetch = jest.fn(); try { await issueVerifiableCredential( "https://some.endpoint", @@ -269,7 +251,7 @@ describe("issueVerifiableCredential", () => { type: ["some-type", "some-other-type"], }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }, ); // eslint-disable-next-line no-empty @@ -293,7 +275,7 @@ describe("issueVerifiableCredential", () => { }); it("handles inline contexts for the claims", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>(); + const mockedFetch = jest.fn(); try { await issueVerifiableCredential( "https://some.endpoint", @@ -306,7 +288,7 @@ describe("issueVerifiableCredential", () => { }, { "@context": ["https://some-credential.context"] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }, ); // eslint-disable-next-line no-empty @@ -332,7 +314,7 @@ describe("issueVerifiableCredential", () => { }); it("doesn't include the subject ID when using the deprecated signature", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>(); + const mockedFetch = jest.fn(); try { await issueVerifiableCredential( "https://some.endpoint", @@ -340,7 +322,7 @@ describe("issueVerifiableCredential", () => { { "@context": ["https://some-subject.context"], aClaim: "a value" }, undefined, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }, ); // eslint-disable-next-line no-empty @@ -363,14 +345,14 @@ describe("issueVerifiableCredential", () => { }); it("doesn't include the subject ID when using the deprecated default signature", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>(); + const mockedFetch = jest.fn(); try { await defaultIssueVerifiableCredential( "https://some.endpoint", "https://some.subject", { "@context": ["https://some-subject.context"], aClaim: "a value" }, undefined, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ); // eslint-disable-next-line no-empty } catch (_e) {} @@ -415,7 +397,7 @@ describe("issueVerifiableCredential", () => { type: ["some-type", "some-other-type"], }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }, ); expect(resultVc.proof.proofValue).toBe( diff --git a/src/issue/issue.ts b/src/issue/issue.ts index f2551bda..067f84df 100644 --- a/src/issue/issue.ts +++ b/src/issue/issue.ts @@ -23,8 +23,6 @@ * @module issue */ -import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; - import type { Iri, JsonLd, @@ -42,7 +40,7 @@ import { import type { ParseOptions } from "../parser/jsonld"; type OptionsType = { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld?: boolean; }; @@ -55,7 +53,7 @@ async function internal_issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims: JsonLd, options: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld: false; } & ParseOptions, ): Promise; @@ -68,7 +66,7 @@ async function internal_issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld?: true; } & ParseOptions, ): Promise; @@ -81,7 +79,7 @@ async function internal_issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld?: boolean; } & ParseOptions, ): Promise; @@ -90,13 +88,13 @@ async function internal_issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld?: boolean; } & ParseOptions, ): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { - internalOptions.fetch = fallbackFetch; + internalOptions.fetch = fetch; } // credentialClaims should contain all the claims, but not the context. @@ -177,7 +175,7 @@ export async function issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims: JsonLd, options: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld: false; }, ): Promise; @@ -205,7 +203,7 @@ export async function issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld?: true; normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, @@ -234,7 +232,7 @@ export async function issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld?: boolean; normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, @@ -248,7 +246,7 @@ export async function issueVerifiableCredential( subjectClaims: JsonLd, credentialClaims?: JsonLd, options?: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld?: true; normalize?: (object: VerifiableCredentialBase) => VerifiableCredentialBase; }, diff --git a/src/lookup/derive.test.ts b/src/lookup/derive.test.ts index c673703f..5e7b478c 100644 --- a/src/lookup/derive.test.ts +++ b/src/lookup/derive.test.ts @@ -19,9 +19,7 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type * as UniversalFetch from "@inrupt/universal-fetch"; -import { Response } from "@inrupt/universal-fetch"; -import { describe, expect, it, jest } from "@jest/globals"; +import { beforeAll, describe, expect, it, jest } from "@jest/globals"; import { mockAccessGrant, mockDefaultPresentation, @@ -32,16 +30,6 @@ import defaultGetVerifilableCredentialAllFromShape, { } from "./derive"; import type * as QueryModule from "./query"; -jest.mock("@inrupt/universal-fetch", () => { - const fetchModule = jest.requireActual( - "@inrupt/universal-fetch", - ) as typeof UniversalFetch; - return { - ...fetchModule, - fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(), - }; -}); - const mockDeriveEndpointDefaultResponse = (anoymous = false) => new Response( JSON.stringify(anoymous ? mockAccessGrant() : mockDefaultPresentation()), @@ -55,6 +43,16 @@ describe.each([ ["named response", mockDeriveEndpointDefaultResponse], ["anonymous response", () => mockDeriveEndpointDefaultResponse(true)], ])("getVerifiableCredentialAllFromShape [%s]", (_, mockResponse) => { + let spiedFetch: jest.Spied; + + beforeAll(() => { + spiedFetch = jest.spyOn(globalThis, "fetch"); + + spiedFetch.mockImplementation(() => { + throw new Error("Unexpected fetch call"); + }); + }); + describe("legacy derive endpoint", () => { it("exposes a default export", async () => { expect(defaultGetVerifilableCredentialAllFromShape).toBe( @@ -74,7 +72,7 @@ describe.each([ credentialSubject: { id: "https://some.subject/" }, }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ); @@ -87,25 +85,20 @@ describe.each([ it.each([[true], [false]])( "defaults to an unauthenticated fetch if no fetch is provided [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; - mockedFetch.fetch.mockResolvedValue(mockResponse()); + spiedFetch.mockResolvedValue(mockResponse()); await getVerifiableCredentialAllFromShape("https://some.endpoint", { "@context": ["https://some.context"], credentialSubject: { id: "https://some.subject/" }, returnLegacyJsonld, }); - expect(mockedFetch.fetch).toHaveBeenCalled(); + expect(spiedFetch).toHaveBeenCalled(); }, ); it.each([[true], [false]])( "includes the expired VC options if requested [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue(mockResponse()); + const mockedFetch = jest.fn(async () => mockResponse()); await getVerifiableCredentialAllFromShape( "https://some.endpoint", { @@ -114,7 +107,7 @@ describe.each([ }, { includeExpiredVc: true, - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ); @@ -131,7 +124,7 @@ describe.each([ "builds a legacy VP request from the provided VC shape", async (returnLegacyJsonld) => { const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() + .fn() .mockResolvedValue(mockResponse()); const queryModule = jest.requireActual("./query") as typeof QueryModule; const spiedQuery = jest.spyOn(queryModule, "query"); @@ -142,7 +135,7 @@ describe.each([ credentialSubject: { id: "https://some.subject/" }, }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ); @@ -163,17 +156,13 @@ describe.each([ ); it("returns the VCs from the obtained VP on a successful response", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue(mockResponse()); - const vc = await getVerifiableCredentialAllFromShape( "https://some.endpoint", { "@context": ["https://some.context"], credentialSubject: { id: "https://some.subject/" }, }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: async () => mockResponse() }, ); expect(vc).toMatchObject( @@ -186,10 +175,6 @@ describe.each([ }); it("returns the VCs from the obtained VP on a successful response [returnLegacyJsonld: false]", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue(mockResponse()); - const vc = await getVerifiableCredentialAllFromShape( "https://some.endpoint", { @@ -197,7 +182,7 @@ describe.each([ credentialSubject: { id: "https://some.webid.provider/strelka" }, }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: async () => mockResponse(), returnLegacyJsonld: false, }, ); @@ -222,19 +207,6 @@ describe.each([ it.each([[true], [false]])( "returns an empty array if the VP contains no VCs [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( - async () => - new Response( - JSON.stringify({ - ...mockDefaultPresentation(), - verifiableCredential: undefined, - }), - { - status: 200, - statusText: "OK", - }, - ), - ); await expect( getVerifiableCredentialAllFromShape( "https://some.endpoint", @@ -243,7 +215,17 @@ describe.each([ credentialSubject: { id: "https://some.subject/" }, }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: async () => + new Response( + JSON.stringify({ + ...mockDefaultPresentation(), + verifiableCredential: undefined, + }), + { + status: 200, + statusText: "OK", + }, + ), returnLegacyJsonld, }, ), @@ -256,9 +238,6 @@ describe.each([ it.each([[true], [false]])( "builds a standard VP request by example from the provided VC shape [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue(mockResponse()); const queryModule = jest.requireActual("./query") as typeof QueryModule; const spiedQuery = jest.spyOn(queryModule, "query"); const VC_SHAPE = { @@ -269,7 +248,7 @@ describe.each([ "https://some.endpoint/query", VC_SHAPE, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: async () => mockResponse(), returnLegacyJsonld, }, ); diff --git a/src/lookup/derive.ts b/src/lookup/derive.ts index 2f7a0ef5..90a308ec 100644 --- a/src/lookup/derive.ts +++ b/src/lookup/derive.ts @@ -19,7 +19,6 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; import type { Iri, VerifiableCredential, @@ -103,7 +102,7 @@ export async function getVerifiableCredentialAllFromShape( holderEndpoint: Iri, vcShape: Partial, options: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; includeExpiredVc?: boolean; returnLegacyJsonld: false; }, @@ -133,7 +132,7 @@ export async function getVerifiableCredentialAllFromShape( holderEndpoint: Iri, vcShape: Partial, options?: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; includeExpiredVc?: boolean; returnLegacyJsonld?: true; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; @@ -164,7 +163,7 @@ export async function getVerifiableCredentialAllFromShape( holderEndpoint: Iri, vcShape: Partial, options?: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; includeExpiredVc?: boolean; returnLegacyJsonld?: boolean; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; @@ -174,13 +173,13 @@ export async function getVerifiableCredentialAllFromShape( holderEndpoint: Iri, vcShape: Partial, options?: { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; includeExpiredVc?: boolean; returnLegacyJsonld?: boolean; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }, ): Promise { - const fetchFn = options?.fetch ?? fallbackFetch; + const fetchFn = options?.fetch ?? fetch; // The request payload depends on the target endpoint. const vpRequest = holderEndpoint.endsWith("/query") ? // The target endpoint is spec-compliant, and uses a standard VP request. diff --git a/src/lookup/query.test.ts b/src/lookup/query.test.ts index ae87ca1f..8b5ccaad 100644 --- a/src/lookup/query.test.ts +++ b/src/lookup/query.test.ts @@ -19,12 +19,8 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import { jest, it, describe, expect, beforeEach } from "@jest/globals"; -import { Response } from "@inrupt/universal-fetch"; -import type * as UniversalFetch from "@inrupt/universal-fetch"; +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { DataFactory } from "n3"; -import type { QueryByExample } from "./query"; -import { query } from "./query"; import { defaultVerifiableClaims, mockAccessGrant, @@ -33,19 +29,10 @@ import { mockPartialPresentation, } from "../common/common.mock"; import { cred, rdf } from "../common/constants"; +import type { QueryByExample } from "./query"; +import { query } from "./query"; const { namedNode } = DataFactory; - -jest.mock("@inrupt/universal-fetch", () => { - const fetchModule = jest.requireActual( - "@inrupt/universal-fetch", - ) as typeof UniversalFetch; - return { - ...fetchModule, - fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(), - }; -}); - const mockRequest: QueryByExample = { type: "QueryByExample", credentialQuery: [ @@ -59,24 +46,34 @@ const mockRequest: QueryByExample = { }; describe("query", () => { + let spiedFetch: jest.Spied; + + beforeEach(() => { + spiedFetch = jest.spyOn(globalThis, "fetch"); + + spiedFetch.mockImplementation(() => { + throw new Error("Unexpected fetch call"); + }); + }); + describe("by example", () => { describe("uses the provided fetch if any", () => { - let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + let mockedFetch: jest.MockedFunction; beforeEach(() => { - mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + mockedFetch = jest.fn( async () => new Response(JSON.stringify(mockDefaultPresentation()), { status: 200, }), - ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + ) as jest.MockedFunction; }); it("returnLegacyJsonld: true", async () => { await query( "https://some.endpoint/query", { query: [mockRequest] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ); expect(mockedFetch).toHaveBeenCalled(); }); @@ -85,7 +82,7 @@ describe("query", () => { "https://some.endpoint/query", { query: [mockRequest] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld: false, }, ); @@ -94,10 +91,10 @@ describe("query", () => { }); describe("resolves when the VP contains no VCs", () => { - let mockedFetch: jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + let mockedFetch: jest.MockedFunction; beforeEach(() => { - mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + mockedFetch = jest.fn( async () => new Response( JSON.stringify( @@ -107,7 +104,7 @@ describe("query", () => { status: 200, }, ), - ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + ) as jest.MockedFunction; }); it("returnLegacyJsonld: true", async () => { @@ -115,7 +112,7 @@ describe("query", () => { query( "https://some.endpoint/query", { query: [mockRequest] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ), ).resolves.toMatchObject({ id: "https://example.org/ns/someCredentialInstance", @@ -128,7 +125,7 @@ describe("query", () => { "https://some.endpoint/query", { query: [mockRequest] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld: false, }, ), @@ -155,13 +152,13 @@ describe("query", () => { ])( "errors if the presentation contains invalid verifiable credentials [%s]", (_, elems) => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => // @ts-expect-error we are intentionall passing invalid VC types here to test for errors new Response(JSON.stringify(mockDefaultPresentation(elems)), { status: 200, }), - ) as jest.MockedFunction<(typeof UniversalFetch)["fetch"]>; + ) as jest.MockedFunction; it.each([[true], [false]])( "[returnLegacyJsonld: %s]", @@ -171,7 +168,7 @@ describe("query", () => { "https://some.endpoint/query", { query: [mockRequest] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -188,10 +185,7 @@ describe("query", () => { ])( "defaults to an unauthenticated fetch if no fetch is provided [args: %s]", async (arg?: { returnLegacyJsonld?: boolean }) => { - const mockedFetch = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; - mockedFetch.fetch.mockResolvedValueOnce( + spiedFetch.mockResolvedValueOnce( new Response(JSON.stringify(mockDefaultPresentation()), { status: 200, }), @@ -201,14 +195,14 @@ describe("query", () => { { query: [mockRequest] }, arg, ); - expect(mockedFetch.fetch).toHaveBeenCalled(); + expect(spiedFetch).toHaveBeenCalled(); }, ); it.each([[true], [false]])( "throws if the given endpoint returns an error [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(undefined, { status: 404, @@ -219,7 +213,7 @@ describe("query", () => { "https://example.org/query", { query: [mockRequest] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -230,7 +224,7 @@ describe("query", () => { it.each([[true], [false]])( "throws if the endpoint responds with a non-JSON payload [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response("Not JSON", { status: 200, @@ -241,7 +235,7 @@ describe("query", () => { "https://example.org/query", { query: [mockRequest] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -271,18 +265,16 @@ describe("query", () => { it.each([[true], [false]])( "posts a request with the appropriate media type [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultPresentation()), { - status: 200, - }), - ); + const mockedFetch = jest.fn().mockResolvedValueOnce( + new Response(JSON.stringify(mockDefaultPresentation()), { + status: 200, + }), + ); await query( "https://some.endpoint/query", { query: [mockRequest] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ); @@ -301,19 +293,17 @@ describe("query", () => { it.each([[true], [false]])( "errors if no presentations exist [returnLegacyJsonld: %s]", async (returnLegacyJsonld) => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify([]), { - status: 200, - }), - ); + const mockedFetch = jest.fn().mockResolvedValueOnce( + new Response(JSON.stringify([]), { + status: 200, + }), + ); await expect( query( "https://some.endpoint/query", { query: [mockRequest] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld, }, ), @@ -331,7 +321,7 @@ describe("query", () => { ); it("returns the VP sent by the endpoint", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify(mockDefaultPresentation()), { status: 200, @@ -341,13 +331,13 @@ describe("query", () => { const vp = await query( "https://example.org/query", { query: [mockRequest] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ); const vpNoLegacy = await query( "https://example.org/query", { query: [mockRequest] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld: false, }, ); @@ -359,7 +349,7 @@ describe("query", () => { }); it("returns the VP sent by the endpoint [using mock access grant]", async () => { - const mockedFetch = jest.fn<(typeof UniversalFetch)["fetch"]>( + const mockedFetch = jest.fn( async () => new Response(JSON.stringify(mockAccessGrant()), { status: 200, @@ -369,13 +359,13 @@ describe("query", () => { const vp = await query( "https://example.org/query", { query: [mockRequest] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ); const vpNoLegacy = await query( "https://example.org/query", { query: [mockRequest] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, returnLegacyJsonld: false, }, ); @@ -414,17 +404,15 @@ describe("query", () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore delete mockedVc.proof.proofValue; - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultPresentation([mockedVc])), { - status: 200, - }), - ); + const mockedFetch = jest.fn().mockResolvedValueOnce( + new Response(JSON.stringify(mockDefaultPresentation([mockedVc])), { + status: 200, + }), + ); const resultVp = await query( "https://example.org/query", { query: [mockRequest] }, - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expect(resultVp.verifiableCredential![0].proof.proofValue).toBe( @@ -450,18 +438,16 @@ describe("query", () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore delete mockedVc.proof.proofValue; - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify(mockDefaultPresentation([mockedVc])), { - status: 200, - }), - ); + const mockedFetch = jest.fn().mockResolvedValueOnce( + new Response(JSON.stringify(mockDefaultPresentation([mockedVc])), { + status: 200, + }), + ); const resultVp = await query( "https://example.org/query", { query: [mockRequest] }, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, normalize(vc) { return { ...vc, diff --git a/src/lookup/query.ts b/src/lookup/query.ts index a2b0806c..db909632 100644 --- a/src/lookup/query.ts +++ b/src/lookup/query.ts @@ -19,7 +19,6 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; import type { DatasetCore } from "@rdfjs/types"; import { DataFactory } from "n3"; import type { @@ -125,7 +124,7 @@ export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, options: ParseOptions & { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld: false; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }, @@ -137,7 +136,7 @@ export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, options?: ParseOptions & { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld?: true; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }, @@ -149,7 +148,7 @@ export async function query( queryEndpoint: Iri, vpRequest: VerifiablePresentationRequest, options?: ParseOptions & { - fetch?: typeof fallbackFetch; + fetch?: typeof fetch; returnLegacyJsonld?: boolean; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }, @@ -159,14 +158,14 @@ export async function query( vpRequest: VerifiablePresentationRequest, options: ParseOptions & Partial<{ - fetch: typeof fallbackFetch; + fetch: typeof fetch; returnLegacyJsonld?: boolean; normalize?: (vc: VerifiableCredentialBase) => VerifiableCredentialBase; }> = {}, ): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { - internalOptions.fetch = fallbackFetch; + internalOptions.fetch = fetch; } const response = await internalOptions.fetch(queryEndpoint, { headers: { diff --git a/src/parser/jsonld.test.ts b/src/parser/jsonld.test.ts index 94373f11..26f92cab 100644 --- a/src/parser/jsonld.test.ts +++ b/src/parser/jsonld.test.ts @@ -19,8 +19,7 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type * as UniversalFetch from "@inrupt/universal-fetch"; -import { describe, expect, it, jest } from "@jest/globals"; +import { describe, expect, it } from "@jest/globals"; import { DataFactory as DF } from "n3"; import { isomorphic } from "rdf-isomorphic"; import { @@ -29,16 +28,6 @@ import { jsonLdToStore, } from "./jsonld"; -jest.mock("@inrupt/universal-fetch", () => { - const fetchModule = jest.requireActual( - "@inrupt/universal-fetch", - ) as typeof UniversalFetch; - return { - ...fetchModule, - fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(), - }; -}); - const data = { "@context": "https://www.w3.org/2018/credentials/v1", id: "https://some.example#credential", diff --git a/src/parser/jsonld.ts b/src/parser/jsonld.ts index ae219213..e6490d43 100644 --- a/src/parser/jsonld.ts +++ b/src/parser/jsonld.ts @@ -20,7 +20,6 @@ // /* eslint-disable max-classes-per-file */ -import { fetch as defaultFetch } from "@inrupt/universal-fetch"; import { promisifyEventEmitter } from "event-emitter-promisify"; import type { IJsonLdContext, @@ -46,7 +45,7 @@ export class CachedFetchDocumentLoader extends FetchDocumentLoader { private readonly allowContextFetching = false, ...args: ConstructorParameters ) { - super(args[0] ?? defaultFetch); + super(args[0] ?? fetch); this.contexts = { ...contexts, ...cachedContexts, ...CONTEXTS }; } @@ -144,14 +143,14 @@ export class CachedJsonLdParser extends JsonLdParser { reusableDocumentLoader ??= new CachedFetchDocumentLoader( undefined, undefined, - defaultFetch, + fetch, ); documentLoader = reusableDocumentLoader; } else { documentLoader = new CachedFetchDocumentLoader( options.contexts, options.allowContextFetching, - defaultFetch, + fetch, ); } diff --git a/src/revoke/revoke.test.ts b/src/revoke/revoke.test.ts index 63aad910..9197170d 100644 --- a/src/revoke/revoke.test.ts +++ b/src/revoke/revoke.test.ts @@ -20,20 +20,12 @@ // import { jest, it, describe, expect } from "@jest/globals"; -import { Response } from "@inrupt/universal-fetch"; -import type * as UniversalFetch from "@inrupt/universal-fetch"; import defaultRevokeVerifiableCredential, { revokeVerifiableCredential, } from "./revoke"; -jest.mock("@inrupt/universal-fetch", () => { - const fetchModule = jest.requireActual( - "@inrupt/universal-fetch", - ) as typeof UniversalFetch; - return { - ...fetchModule, - fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(), - }; +const spiedFetch = jest.spyOn(globalThis, "fetch").mockImplementation(() => { + throw new Error("Unexpected fetch call"); }); describe("revokeVerifiableCredential", () => { @@ -47,7 +39,7 @@ describe("revokeVerifiableCredential", () => { "https://some.endpoint", "https://some.example#credential", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }, ); // eslint-disable-next-line no-empty @@ -56,9 +48,6 @@ describe("revokeVerifiableCredential", () => { }); it("defaults to an unauthenticated fetch if no fetch is provided", async () => { - const mockedFetch = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; try { await revokeVerifiableCredential( "https://some.endpoint", @@ -66,41 +55,37 @@ describe("revokeVerifiableCredential", () => { ); // eslint-disable-next-line no-empty } catch (_e) {} - expect(mockedFetch.fetch).toHaveBeenCalled(); + expect(spiedFetch).toHaveBeenCalled(); }); it("throws if the issuer returns an error", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue( - new Response(undefined, { - status: 400, - statusText: "Bad request", - }), - ); + const mockedFetch = jest.fn().mockResolvedValue( + new Response(undefined, { + status: 400, + statusText: "Bad request", + }), + ); await expect( revokeVerifiableCredential( "https://some.endpoint", "https://some.example#credential", - { fetch: mockedFetch as (typeof UniversalFetch)["fetch"] }, + { fetch: mockedFetch }, ), ).rejects.toThrow(/some.endpoint.*400.*Bad request/); }); it("sends an appropriate revocation request to the issuer", async () => { - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValue( - new Response(undefined, { - status: 200, - statusText: "OK", - }), - ); + const mockedFetch = jest.fn().mockResolvedValue( + new Response(undefined, { + status: 200, + statusText: "OK", + }), + ); await revokeVerifiableCredential( "https://some.endpoint", "https://some.example#credential", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }, ); expect(mockedFetch).toHaveBeenCalledWith( diff --git a/src/revoke/revoke.ts b/src/revoke/revoke.ts index 8148df12..a898d818 100644 --- a/src/revoke/revoke.ts +++ b/src/revoke/revoke.ts @@ -22,7 +22,6 @@ /** * @module revoke */ -import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; import type { Iri } from "../common/common"; /** @@ -48,7 +47,7 @@ export async function revokeVerifiableCredential( ): Promise { const internalOptions = { ...options }; if (internalOptions.fetch === undefined) { - internalOptions.fetch = fallbackFetch; + internalOptions.fetch = fetch; } const response = await internalOptions.fetch(issuerEndpoint, { method: "POST", diff --git a/src/verify/verify.test.ts b/src/verify/verify.test.ts index dac4f7db..d60b3fb5 100644 --- a/src/verify/verify.test.ts +++ b/src/verify/verify.test.ts @@ -19,8 +19,6 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type * as UniversalFetch from "@inrupt/universal-fetch"; -import { Response } from "@inrupt/universal-fetch"; import { describe, expect, it, jest } from "@jest/globals"; import { mocked } from "jest-mock"; import { DataFactory, Store } from "n3"; @@ -57,14 +55,9 @@ jest.mock("../common/common", () => { getVerifiableCredentialApiConfiguration: jest.fn(), }; }); -jest.mock("@inrupt/universal-fetch", () => { - const fetchModule = jest.requireActual( - "@inrupt/universal-fetch", - ) as typeof UniversalFetch; - return { - ...fetchModule, - fetch: jest.fn<(typeof UniversalFetch)["fetch"]>(), - }; + +const spiedFetch = jest.spyOn(globalThis, "fetch").mockImplementation(() => { + throw new Error("Unexpected fetch call"); }); const MOCK_VC = { @@ -102,11 +95,7 @@ describe("isValidVc", () => { const MOCK_VERIFY_RESPONSE = { checks: [], warning: [], errors: [] }; it("falls back to an unauthenticated fetch if none is provided", async () => { - const mockedFetch = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; - - mockedFetch.fetch.mockResolvedValueOnce( + spiedFetch.mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), @@ -115,17 +104,12 @@ describe("isValidVc", () => { verificationEndpoint: MOCK_VERIFY_ENDPOINT, }); - expect(mockedFetch.fetch).toHaveBeenCalled(); + expect(spiedFetch).toHaveBeenCalled(); }); it("discovers the verification endpoint if none is provided", async () => { - // Use the fetch fallback on purpose to check that no option may be passed - // to the function. - const mockedFetch = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; // First, the VC is fetche - mockedFetch.fetch + spiedFetch .mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VC), { status: 200 }), ) @@ -148,14 +132,14 @@ describe("isValidVc", () => { }); it("uses the provided fetch if any", async () => { - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), ); await isValidVc(MOCK_VC, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, }); @@ -163,7 +147,7 @@ describe("isValidVc", () => { }); it("uses the provided fetch if any on RDFJS input", async () => { - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), @@ -174,7 +158,7 @@ describe("isValidVc", () => { id: MOCK_VC.id, }), { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, }, ); @@ -189,14 +173,14 @@ describe("isValidVc", () => { specCompliant: {}, }); - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), ); await isValidVc(MOCK_VC, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }); expect(mockedFetch).toHaveBeenCalledWith( @@ -222,7 +206,7 @@ describe("isValidVc", () => { ); await isValidVc("https://example.com/someVc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, }); @@ -243,7 +227,7 @@ describe("isValidVc", () => { ); await expect( isValidVc("https://example.com/someVc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, }), ).rejects.toThrow( @@ -261,8 +245,7 @@ describe("isValidVc", () => { isValidVc( { ...MOCK_VC, id: undefined as unknown as string }, { - fetch: - mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, }, ), @@ -277,7 +260,7 @@ describe("isValidVc", () => { .mockResolvedValueOnce(new Response("Not a valid JSON.")); await expect( isValidVc("https://example.com/someVc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, }), ).rejects.toThrow( @@ -295,7 +278,7 @@ describe("isValidVc", () => { await expect( isValidVc("https://example.com/someVc", { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, }), ).rejects.toThrow( @@ -304,14 +287,14 @@ describe("isValidVc", () => { }); it("uses the provided verification endpoint if any", async () => { - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), ); await isValidVc(MOCK_VC, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, verificationEndpoint: "https://some.verification.api", }); expect(mockedFetch).toHaveBeenCalledWith( @@ -342,7 +325,7 @@ describe("isValidVc", () => { await expect( isValidVc(MOCK_VC, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }), ).rejects.toThrow( `The VC service provider ${MOCK_VC.issuer} does not advertize for a verifier service in its .well-known/vc-configuration document`, @@ -350,7 +333,7 @@ describe("isValidVc", () => { }); it("throws if the verification endpoint returns an error", async () => { - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(undefined, { status: 400, statusText: "Bad request", @@ -359,7 +342,7 @@ describe("isValidVc", () => { await expect( isValidVc(MOCK_VC, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, }), ).rejects.toThrow(/consent\.example\.com.*400 Bad request/); @@ -372,14 +355,14 @@ describe("isValidVc", () => { await expect( isValidVc(MOCK_VC, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, verificationEndpoint: MOCK_VERIFY_ENDPOINT, }), ).rejects.toThrow(/Parsing.*consent\.example\.com/); }); it("returns the validation result from the access endpoint", async () => { - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), @@ -387,7 +370,7 @@ describe("isValidVc", () => { await expect( isValidVc(MOCK_VC, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, verificationEndpoint: "https://some.verification.api", }), ).resolves.toEqual({ checks: [], errors: [], warning: [] }); @@ -404,11 +387,7 @@ describe("isValidVerifiable Presentation", () => { const MOCK_VERIFY_RESPONSE = { checks: [], warning: [], errors: [] }; it("falls back to the embedded fetch if none is provided", async () => { - const mockedFetch = jest.requireMock( - "@inrupt/universal-fetch", - ) as jest.Mocked; - // mocked(isVerifi).mockReturnValueOnce(true); - mockedFetch.fetch.mockResolvedValueOnce( + spiedFetch.mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), @@ -417,18 +396,18 @@ describe("isValidVerifiable Presentation", () => { isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP), ).resolves.toMatchObject({ errors: [] }); - expect(mockedFetch.fetch).toHaveBeenCalled(); + expect(spiedFetch).toHaveBeenCalled(); }); it("uses the provided fetch if any", async () => { - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), ); mocked(isVerifiablePresentation).mockReturnValueOnce(true); await isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, domain: "domain", challenge: "challenge", }); @@ -438,14 +417,14 @@ describe("isValidVerifiable Presentation", () => { it("sends the given vp to the verify endpoint", async () => { mocked(isVerifiablePresentation).mockReturnValueOnce(true); - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), ); await isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, domain: "domain", challenge: "challenge", }); @@ -462,7 +441,7 @@ describe("isValidVerifiable Presentation", () => { }); it("uses the provided verification endpoint if any", async () => { - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), @@ -473,7 +452,7 @@ describe("isValidVerifiable Presentation", () => { "https://some.verification.api", MOCK_VP, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }, ); expect(mockedFetch).toHaveBeenCalledWith( @@ -493,16 +472,14 @@ describe("isValidVerifiable Presentation", () => { }); mocked(isVerifiablePresentation).mockReturnValueOnce(true); - const mockedFetch = jest - .fn<(typeof UniversalFetch)["fetch"]>() - .mockResolvedValueOnce( - new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { - status: 200, - }), - ); + const mockedFetch = jest.fn().mockResolvedValueOnce( + new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { + status: 200, + }), + ); await isValidVerifiablePresentation(null, MOCK_VP, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }); expect(mockedDiscovery).toHaveBeenCalledWith(MOCK_VP.holder); }); @@ -513,7 +490,7 @@ describe("isValidVerifiable Presentation", () => { specCompliant: {}, }); mocked(isVerifiablePresentation).mockReturnValueOnce(true); - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), @@ -521,7 +498,7 @@ describe("isValidVerifiable Presentation", () => { await expect( isValidVerifiablePresentation(null, MOCK_VP, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"], + fetch: mockedFetch, }), ).rejects.toThrow( `The VC service provider ${MOCK_VP.holder} does not advertize for a verifier service in its .well-known/vc-configuration document`, @@ -529,7 +506,7 @@ describe("isValidVerifiable Presentation", () => { }); it("throws if passed VP is not a verifiable presentation [because the VC has no id]", async () => { - const mockedFetch = jest.fn(global.fetch); + const mockedFetch = jest.fn(); mocked(isVerifiablePresentation).mockReturnValueOnce(false); const MOCK_VP_NO_ID = { @@ -544,7 +521,7 @@ describe("isValidVerifiable Presentation", () => { await expect( isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP_NO_ID, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, }), ).rejects.toThrow( `Expected vc.id to be a string, found [undefined] of type [undefined] on ${JSON.stringify( @@ -559,7 +536,7 @@ describe("isValidVerifiable Presentation", () => { }); it("throws if passed VP is not a verifiable presentation [because it is the wrong type]", async () => { - const mockedFetch = jest.fn(global.fetch); + const mockedFetch = jest.fn(); mocked(isVerifiablePresentation).mockReturnValueOnce(false); const MOCK_VP_NO_ID = Object.assign( @@ -577,13 +554,13 @@ describe("isValidVerifiable Presentation", () => { await expect( isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP_NO_ID, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, }), ).rejects.toThrow("Expected exactly one Verifiable Presentation. Found 0."); }); it("throws if passed VP is not a verifiable presentation [because it has too many holders]", async () => { - const mockedFetch = jest.fn(global.fetch); + const mockedFetch = jest.fn(); mocked(isVerifiablePresentation).mockReturnValueOnce(false); const MOCK_VP_EXTRA_HOLDER = Object.assign( @@ -606,8 +583,7 @@ describe("isValidVerifiable Presentation", () => { MOCK_VERIFY_ENDPOINT, MOCK_VP_EXTRA_HOLDER, { - fetch: - mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, }, ), ).rejects.toThrow( @@ -616,7 +592,7 @@ describe("isValidVerifiable Presentation", () => { }); it("throws if passed VP is not a verifiable presentation [included VC is missing type]", async () => { - const mockedFetch = jest.fn(global.fetch); + const mockedFetch = jest.fn(); mocked(isVerifiablePresentation).mockReturnValueOnce(false); const VC_STORE = await jsonLdStringToStore(JSON.stringify(MOCK_VC)); @@ -633,8 +609,7 @@ describe("isValidVerifiable Presentation", () => { MOCK_VERIFY_ENDPOINT, MOCK_VP_EXTRA_HOLDER, { - fetch: - mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, }, ), ).rejects.toThrow( @@ -642,7 +617,7 @@ describe("isValidVerifiable Presentation", () => { ); await expect( isValidVc(Object.assign(VC_STORE, { id: MOCK_VC.id }), { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, }), ).rejects.toThrow( "The request to [[object Object]] returned an unexpected response", @@ -650,7 +625,7 @@ describe("isValidVerifiable Presentation", () => { }); it("throws if passed VP is not a verifiable presentation [because it has an invalid subject]", async () => { - const mockedFetch = jest.fn(global.fetch); + const mockedFetch = jest.fn(); mocked(isVerifiablePresentation).mockReturnValueOnce(false); const MOCK_VP_NO_TYPE = new Store([ @@ -661,7 +636,7 @@ describe("isValidVerifiable Presentation", () => { await expect( // @ts-expect-error the vp is also missing the verifiableCredential property isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP_NO_TYPE, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, }), ).rejects.toThrow( "Expected VP subject to be NamedNode or BlankNode. Instead found [vp subpect] with termType [Literal]", @@ -669,7 +644,7 @@ describe("isValidVerifiable Presentation", () => { }); it("throws if response is not valid JSON", async () => { - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(`some non-JSON response`, { status: 200, }), @@ -678,7 +653,7 @@ describe("isValidVerifiable Presentation", () => { await expect( isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, }), ).rejects.toThrow( `Parsing the response of the verification service hosted at [${MOCK_VERIFY_ENDPOINT}] as JSON failed:`, @@ -686,7 +661,7 @@ describe("isValidVerifiable Presentation", () => { }); it("throws if the verification endpoint returns an error", async () => { - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(undefined, { status: 400, statusText: "Bad request", @@ -696,13 +671,13 @@ describe("isValidVerifiable Presentation", () => { await expect( isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, }), ).rejects.toThrow(/consent\.example\.com.*400 Bad request/); }); it("returns the validation result from the verification endpoint", async () => { - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( + const mockedFetch = jest.fn().mockResolvedValueOnce( new Response(JSON.stringify(MOCK_VERIFY_RESPONSE), { status: 200, }), @@ -710,7 +685,7 @@ describe("isValidVerifiable Presentation", () => { mocked(isVerifiablePresentation).mockReturnValueOnce(true); await expect( isValidVerifiablePresentation(MOCK_VERIFY_ENDPOINT, MOCK_VP, { - fetch: mockedFetch as (typeof UniversalFetch)["fetch"] as typeof fetch, + fetch: mockedFetch as typeof fetch, }), ).resolves.toEqual({ checks: [], errors: [], warning: [] }); }); diff --git a/src/verify/verify.ts b/src/verify/verify.ts index 6e866013..41779503 100644 --- a/src/verify/verify.ts +++ b/src/verify/verify.ts @@ -24,7 +24,6 @@ */ import type { UrlString } from "@inrupt/solid-client"; -import { fetch as fallbackFetch } from "@inrupt/universal-fetch"; import type { DatasetCore } from "@rdfjs/types"; import { DataFactory } from "n3"; @@ -97,7 +96,7 @@ export async function isValidVc( }> & ParseOptions, ): Promise<{ checks: string[]; warnings: string[]; errors: string[] }> { - const fetcher = options?.fetch ?? fallbackFetch; + const fetcher = options?.fetch ?? fetch; const vcObject = await dereferenceVc(vc, options); @@ -202,7 +201,7 @@ export async function isValidVerifiablePresentation( challenge: string; }> = {}, ): Promise<{ checks: string[]; warnings: string[]; errors: string[] }> { - const fetcher = options.fetch ?? fallbackFetch; + const fetcher = options.fetch ?? fetch; const dataset = await asDataset(verifiablePresentation, false); const subject = getVpSubject(dataset); From 5dd559a66ce6d14462469d472fab6f439b5af9de Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 17 Dec 2023 21:54:39 +0000 Subject: [PATCH 78/79] chore: drop node 16 tests --- .github/workflows/ci.yml | 2 +- .github/workflows/e2e-node.yml | 2 +- README.md | 2 +- package-lock.json | 2 +- package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b425a569..63b05f42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: matrix: # Available OS's: https://help.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [20.x, 18.x, 16.x] + node-version: [20.x, 18.x] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} diff --git a/.github/workflows/e2e-node.yml b/.github/workflows/e2e-node.yml index 1550c47c..2c7ae695 100644 --- a/.github/workflows/e2e-node.yml +++ b/.github/workflows/e2e-node.yml @@ -14,7 +14,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - node-version: ["16.x", "18.x", "20.x"] + node-version: ["18.x", "20.x"] environment-name: ["ESS PodSpaces", "ESS Dev-2-1"] experimental: [false] include: diff --git a/README.md b/README.md index 5036f4ed..4260e45a 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ module](https://www.npmjs.com/package/buffer). ### Node.js Support Our JavaScript Client Libraries track Node.js [LTS -releases](https://nodejs.org/en/about/releases/), and support 16.x, 18.x and 20.x. +releases](https://nodejs.org/en/about/releases/), and support 18.x and 20.x. ## Changelog diff --git a/package-lock.json b/package-lock.json index b08dc78e..2627a204 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,7 @@ "typescript": "^5.0.4" }, "engines": { - "node": "^16.0.0 || ^18.0.0 || ^20.0.0" + "node": "^18.0.0 || ^20.0.0" }, "optionalDependencies": { "fsevents": "^2.3.2" diff --git a/package.json b/package.json index 90bbff23..ad1596db 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,6 @@ "access": "public" }, "engines": { - "node": "^16.0.0 || ^18.0.0 || ^20.0.0" + "node": "^18.0.0 || ^20.0.0" } } From 212a1aaf88dfbfc25171b898c76d4a698bba536b Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 17 Dec 2023 23:23:38 +0000 Subject: [PATCH 79/79] chore: remove polyfills in e2e tests --- jest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/jest.config.ts b/jest.config.ts index 9b2b8599..18c9066e 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -56,6 +56,7 @@ export default { roots: ["/e2e/node"], setupFiles: ["/e2e/node/jest.e2e.setup.ts"], slowTestThreshold: 30, + setupFilesAfterEnv: [], }, ], } as Config;