diff --git a/package-lock.json b/package-lock.json index a3e50cfc7..aa6e806a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@inrupt/solid-client": "^1.25.1", - "@inrupt/solid-client-vc": "^0.8.0-beta.1", + "@inrupt/solid-client-vc": "file:../solid-client-vc-js", "@inrupt/universal-fetch": "^1.0.1", "@types/rdfjs__dataset": "^1.0.5", "auth-header": "^1.0.0", @@ -41,6 +41,7 @@ "jest-environment-jsdom": "^29.3.1", "license-checker": "^25.0.1", "prettier": "^3.0.2", + "rdf-isomorphic": "^1.3.1", "rollup": "^3.1.0", "rollup-plugin-typescript2": "^0.35.0", "ts-jest": "^29.0.3", @@ -57,6 +58,59 @@ "fsevents": "^2.3.2" } }, + "../solid-client-vc-js": { + "name": "@inrupt/solid-client-vc", + "version": "0.8.0-beta.1", + "license": "MIT", + "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", + "jsonld-streaming-serializer": "^2.1.0", + "md5": "^2.3.0", + "n3": "^1.17.0", + "rdf-namespaces": "^1.12.0" + }, + "devDependencies": { + "@inrupt/eslint-config-lib": "^2.0.0", + "@inrupt/internal-playwright-helpers": "^2.0.4", + "@inrupt/internal-test-env": "^2.0.4", + "@inrupt/jest-jsdom-polyfills": "^2.0.4", + "@playwright/test": "^1.28.1", + "@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", + "@types/n3": "^1.16.0", + "@types/node": "^20.1.2", + "@types/rdfjs__dataset": "2.0.7", + "dotenv-flow": "^3.2.0", + "eslint": "^8.18.0", + "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", + "ts-node": "^10.9.1", + "typedoc": "^0.25.1", + "typedoc-plugin-markdown": "^3.15.1", + "typescript": "^5.0.4" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || ^20.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -1051,27 +1105,8 @@ } }, "node_modules/@inrupt/solid-client-vc": { - "version": "0.8.0-beta.1", - "resolved": "https://registry.npmjs.org/@inrupt/solid-client-vc/-/solid-client-vc-0.8.0-beta.1.tgz", - "integrity": "sha512-LZh8HhHNmdQfdEWx8Z4PgBFbbTe+5rzJekQYHYEwkaV6MIPw7vHszfej3rHRK8D0wRVSnt8lg5cTw9EgxrFckQ==", - "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", - "jsonld-streaming-serializer": "^2.1.0", - "md5": "^2.3.0", - "n3": "^1.17.0", - "rdf-namespaces": "^1.12.0" - }, - "engines": { - "node": "^16.0.0 || ^18.0.0 || ^20.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } + "resolved": "../solid-client-vc-js", + "link": true }, "node_modules/@inrupt/universal-fetch": { "version": "1.0.3", @@ -3303,14 +3338,6 @@ "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", @@ -3438,6 +3465,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" } @@ -3541,14 +3569,6 @@ "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.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", @@ -4961,7 +4981,8 @@ "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==" + "integrity": "sha512-uyHG8gjwYGDlKoo0Txtx/u1HI1ubj0FK0rVqI4O0s1EymQm4iAEMbrS5B+XFlSaS8SZ3xzoKX+YHRZk8Nk/bXg==", + "dev": true }, "node_modules/event-target-shim": { "version": "5.0.1", @@ -5755,6 +5776,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/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -6047,11 +6078,6 @@ "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", @@ -7265,18 +7291,6 @@ "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/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -7550,16 +7564,6 @@ "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", @@ -7654,6 +7658,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", @@ -8871,6 +8881,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", @@ -8884,6 +8906,27 @@ "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", + "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 0bbf98045..0e2080ebc 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "jest-environment-jsdom": "^29.3.1", "license-checker": "^25.0.1", "prettier": "^3.0.2", + "rdf-isomorphic": "^1.3.1", "rollup": "^3.1.0", "rollup-plugin-typescript2": "^0.35.0", "ts-jest": "^29.0.3", @@ -134,7 +135,7 @@ }, "dependencies": { "@inrupt/solid-client": "^1.25.1", - "@inrupt/solid-client-vc": "^0.8.0-beta.1", + "@inrupt/solid-client-vc": "file:../solid-client-vc-js", "@inrupt/universal-fetch": "^1.0.1", "@types/rdfjs__dataset": "^1.0.5", "auth-header": "^1.0.0", diff --git a/src/common/constants.ts b/src/common/constants.ts index e6f2e2497..c177ea4d1 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -29,9 +29,14 @@ const XSD = "http://www.w3.org/2001/XMLSchema#"; export const XSD_BOOLEAN = namedNode(`${XSD}boolean`); -export const SOLID_ACCESS_GRANT = namedNode(`${VC}SolidAccessGrant`); export const TYPE = namedNode(rdf.type); +export const solidVc = { + SolidAccessRequest: namedNode(`${VC}SolidAccessRequest`), + SolidAccessGrant: namedNode(`${VC}SolidAccessGrant`), + SolidAccessDenial: namedNode(`${VC}SolidAccessDenial`), +} + export const gc = { providedConsent: namedNode(`${GC}providedConsent`), hasConsent: namedNode(`${GC}hasConsent`), diff --git a/src/common/getters.ts b/src/common/getters.ts index 39912479a..898c094b5 100644 --- a/src/common/getters.ts +++ b/src/common/getters.ts @@ -32,6 +32,7 @@ import { getIssuer, getIssuanceDate, getExpirationDate, + DatasetWithId, } from "@inrupt/solid-client-vc"; import type { AccessGrantGConsent } from "../gConsent/type/AccessGrant"; import type { AccessRequestGConsent } from "../gConsent/type/AccessRequest"; @@ -127,7 +128,7 @@ export { /** * @internal */ -export function getConsent(vc: AccessGrantGConsent | AccessRequestGConsent) { +export function getConsent(vc: DatasetWithId) { const credentialSubject = getCredentialSubject(vc); const consents = [ ...vc.match(credentialSubject, gc.providedConsent, null, defaultGraph()), @@ -160,7 +161,7 @@ export function getConsent(vc: AccessGrantGConsent | AccessRequestGConsent) { * @returns The resources IRIs */ export function getResources( - vc: AccessGrantGConsent | AccessRequestGConsent, + vc: DatasetWithId, ): string[] { const resources: string[] = []; @@ -194,7 +195,7 @@ export function getResources( * @returns The purpose IRIs */ export function getPurposes( - vc: AccessGrantGConsent | AccessRequestGConsent, + vc: DatasetWithId, ): string[] { const consent = getConsent(vc); const purposes: string[] = []; @@ -217,7 +218,7 @@ export function getPurposes( } export function isGConsentAccessGrant( - vc: AccessGrantGConsent | AccessRequestGConsent, + vc: DatasetWithId, ): boolean { const credentialSubject = getCredentialSubject(vc); const providedConsent = getSingleObject( @@ -263,10 +264,10 @@ export function isGConsentAccessGrant( */ export function getResourceOwner(vc: AccessGrantGConsent): string; export function getResourceOwner( - vc: AccessGrantGConsent | AccessRequestGConsent, + vc: DatasetWithId, ): string | undefined; export function getResourceOwner( - vc: AccessGrantGConsent | AccessRequestGConsent, + vc: DatasetWithId, ): string | undefined { const credentialSubject = getCredentialSubject(vc); if (isGConsentAccessGrant(vc)) { @@ -294,7 +295,7 @@ export function getResourceOwner( * @returns The requestor WebID */ export function getRequestor( - vc: AccessGrantGConsent | AccessRequestGConsent, + vc: DatasetWithId, ): string { const credentialSubject = getCredentialSubject(vc); const providedConsent = getSingleObject( @@ -324,7 +325,7 @@ export function getRequestor( * @returns The access modes the grant recipient can exercise. */ export function getAccessModes( - vc: AccessGrantGConsent | AccessRequestGConsent, + vc: DatasetWithId, ): AccessModes { const consent = getConsent(vc); return { @@ -357,7 +358,7 @@ const shorthand = { * @returns The VC types */ export function getTypes( - vc: AccessGrantGConsent | AccessRequestGConsent, + vc: DatasetWithId, ): string[] { const results = [ ...vc.match(namedNode(getId(vc)), TYPE, undefined, defaultGraph()), @@ -393,7 +394,7 @@ export function getTypes( * @returns true if the Grant applies to contained resources, false otherwise. */ export function getInherit( - vc: AccessGrantGConsent | AccessRequestGConsent, + vc: DatasetWithId, ): boolean { return !vc.has( quad( @@ -418,9 +419,9 @@ export function getInherit( * ``` */ export class AccessGrantWrapper { - private vc: AccessGrantGConsent | AccessRequestGConsent; + private vc: DatasetWithId; - constructor(vc: AccessGrantGConsent | AccessRequestGConsent) { + constructor(vc: DatasetWithId) { this.vc = vc; } diff --git a/src/gConsent/guard/isAccessGrant.ts b/src/gConsent/guard/isAccessGrant.ts index f4be56775..4c62de9b5 100644 --- a/src/gConsent/guard/isAccessGrant.ts +++ b/src/gConsent/guard/isAccessGrant.ts @@ -32,7 +32,7 @@ export const GC_CONSENT_STATUS_EXPLICITLY_GIVEN = export const GC_CONSENT_STATUS_REQUESTED = "https://w3id.org/GConsent#ConsentStatusRequested"; -// Implemented as isGConsentAccessGrant in src/common/getters +// Implemented as isGConsentAccessGrant in src/common/getters as isGConsentAccessGrant export function isAccessGrant( vc: T, ): vc is T & AccessGrantBody { diff --git a/src/gConsent/guard/isBaseAccessGrantVerifiableCredential.test.ts b/src/gConsent/guard/isBaseAccessGrantVerifiableCredential.test.ts index 106fe1380..845218470 100644 --- a/src/gConsent/guard/isBaseAccessGrantVerifiableCredential.test.ts +++ b/src/gConsent/guard/isBaseAccessGrantVerifiableCredential.test.ts @@ -20,7 +20,8 @@ // import { it, describe, expect } from "@jest/globals"; -import { isBaseAccessGrantVerifiableCredential } from "./isBaseAccessGrantVerifiableCredential"; +import { isBaseAccessGrantVerifiableCredential, isRdfjsBaseAccessGrantVerifiableCredential } from "./isBaseAccessGrantVerifiableCredential"; +import { verifiableCredentialToDataset } from "@inrupt/solid-client-vc"; const validAccessGrantVerifiableCredential = { "@context": [ @@ -55,45 +56,70 @@ const validAccessGrantVerifiableCredential = { }; describe("isBaseAccessGrantVerifiableCredential", () => { - it("Returns true on valid Access Grant VC", () => { + it("Returns true on valid Access Grant VC", async () => { expect( isBaseAccessGrantVerifiableCredential( validAccessGrantVerifiableCredential, ), ).toBe(true); + expect( + isRdfjsBaseAccessGrantVerifiableCredential( + await verifiableCredentialToDataset(validAccessGrantVerifiableCredential), + ), + ).toBe(true); }); - it("Returns false on invalid Access Grant VC", () => { - expect( - isBaseAccessGrantVerifiableCredential({ - ...validAccessGrantVerifiableCredential, - credentialSubject: { - ...validAccessGrantVerifiableCredential.credentialSubject, - providedConsent: { - ...validAccessGrantVerifiableCredential.credentialSubject - .providedConsent, - forPersonalData: ["https://example.pod/resourceX", {}], - }, + it("Returns false on invalid Access Grant VC", async () => { + const vc = { + ...validAccessGrantVerifiableCredential, + credentialSubject: { + ...validAccessGrantVerifiableCredential.credentialSubject, + providedConsent: { + ...validAccessGrantVerifiableCredential.credentialSubject + .providedConsent, + forPersonalData: ["https://example.pod/resourceX", {}], }, - }), + }, + } + expect( + isBaseAccessGrantVerifiableCredential(vc), + ).toBe(false); + expect( + isRdfjsBaseAccessGrantVerifiableCredential(await verifiableCredentialToDataset(vc)), ).toBe(false); }); - it("Returns false on invalid issuance date", () => { + it("Returns false on invalid issuance date", async () => { + const vc = { + ...validAccessGrantVerifiableCredential, + issuanceDate: [], + }; expect( - isBaseAccessGrantVerifiableCredential({ - ...validAccessGrantVerifiableCredential, - issuanceDate: [], - }), + isBaseAccessGrantVerifiableCredential(vc), ).toBe(false); + expect( + isRdfjsBaseAccessGrantVerifiableCredential(await verifiableCredentialToDataset(vc)), + ).toBe(false); + }); + + it("Returns true on undefined issuance date", async () => { + const vc = { + ...validAccessGrantVerifiableCredential, + issuanceDate: undefined, + } + expect( + isBaseAccessGrantVerifiableCredential(vc), + ).toBe(true); }); - it("Returns true on undefined issuance date", () => { + // FIXME: Work out why undefined issuance dates are allowed when they are required in the VC type + it.skip("Returns true on undefined issuance date in RDFJS", async () => { + const vc = { + ...validAccessGrantVerifiableCredential, + issuanceDate: undefined, + } expect( - isBaseAccessGrantVerifiableCredential({ - ...validAccessGrantVerifiableCredential, - issuanceDate: undefined, - }), + isRdfjsBaseAccessGrantVerifiableCredential(await verifiableCredentialToDataset(vc)), ).toBe(true); }); }); diff --git a/src/gConsent/guard/isBaseAccessGrantVerifiableCredential.ts b/src/gConsent/guard/isBaseAccessGrantVerifiableCredential.ts index 2b8ae8b20..489cbc6dc 100644 --- a/src/gConsent/guard/isBaseAccessGrantVerifiableCredential.ts +++ b/src/gConsent/guard/isBaseAccessGrantVerifiableCredential.ts @@ -26,8 +26,15 @@ import type { RequestCredentialSubject, RequestCredentialSubjectPayload, } from "../type/AccessVerifiableCredential"; -import { isGConsentAttributes } from "./isGConsentAttributes"; +import { isGConsentAttributes, isRdfjsGConsentAttributes } from "./isGConsentAttributes"; import { isBaseAccessVcBody } from "./isBaseAccessVcBody"; +import { DatasetWithId, getCredentialSubject, getExpirationDate, getId, getIssuanceDate, getIssuer } from "@inrupt/solid-client-vc"; +import { TYPE, gc, solidVc } from "../../common/constants"; +import { DataFactory } from "n3"; +import { NamedNode } from "@rdfjs/types"; +import { getAccessModes, getResources } from "../../common"; +import { getConsent, getPurposes } from "../../common/getters"; +const { namedNode, quad } = DataFactory; function isGrantCredentialSubject( x: @@ -48,3 +55,18 @@ export function isBaseAccessGrantVerifiableCredential( isGConsentAttributes(x.credentialSubject.providedConsent) ); } + +export function isRdfjsBaseAccessGrantVerifiableCredential(data: DatasetWithId) { + const s = namedNode(getId(data)); + if(!data.has(quad(s, TYPE, solidVc.SolidAccessDenial)) && !data.has(quad(s, TYPE, solidVc.SolidAccessGrant)) && !data.has(quad(s, TYPE, solidVc.SolidAccessRequest))) { + return false; + } + + // getConsent and getIssuanceDate can error + try { + getIssuanceDate(data); + return isRdfjsGConsentAttributes(data, getConsent(data)) + } catch (e) { + return false; + } +} diff --git a/src/gConsent/guard/isGConsentAttributes.ts b/src/gConsent/guard/isGConsentAttributes.ts index a5227b9c9..2d319447f 100644 --- a/src/gConsent/guard/isGConsentAttributes.ts +++ b/src/gConsent/guard/isGConsentAttributes.ts @@ -72,6 +72,7 @@ export function isRdfjsGConsentAttributes( if ( ![acl.Append, acl.Read, acl.Write].some((mode) => mode.equals(object)) ) { + console.log('mode issue') return false; } } @@ -82,7 +83,7 @@ export function isRdfjsGConsentAttributes( ]; if ( statuses.length !== 1 || - [ + ![ gc.ConsentStatusDenied, gc.ConsentStatusExplicitlyGiven, gc.ConsentStatusRequested, @@ -97,7 +98,7 @@ export function isRdfjsGConsentAttributes( null, defaultGraph(), )) { - if (object.termType === "NamedNode") { + if (object.termType !== "NamedNode") { return false; } } diff --git a/src/gConsent/manage/approveAccessRequest.ts b/src/gConsent/manage/approveAccessRequest.ts index 1620356d5..77b028fcf 100644 --- a/src/gConsent/manage/approveAccessRequest.ts +++ b/src/gConsent/manage/approveAccessRequest.ts @@ -22,7 +22,7 @@ import type { UrlString, WebId } from "@inrupt/solid-client"; // eslint-disable-next-line camelcase import { acp_ess_2 } from "@inrupt/solid-client"; -import type { VerifiableCredential } from "@inrupt/solid-client-vc"; +import type { VerifiableCredential, VerifiableCredentialBase } from "@inrupt/solid-client-vc"; import type { AccessBaseOptions } from "../type/AccessBaseOptions"; import type { AccessGrantBody } from "../type/AccessVerifiableCredential"; import type { AccessGrantParameters } from "../type/Parameter"; @@ -58,7 +58,7 @@ export type ApproveAccessRequestOverrides = Omit< * @param accessGrant The grant returned by the VC issuer * @returns An equivalent JSON-LD document framed according to our typing. */ -export function normalizeAccessGrant( +export function normalizeAccessGrant( accessGrant: T, ): T { // Proper type checking is performed after normalization, so casting here is fine. diff --git a/src/gConsent/manage/getAccessGrant.test.ts b/src/gConsent/manage/getAccessGrant.test.ts index a166f0135..8cccb616c 100644 --- a/src/gConsent/manage/getAccessGrant.test.ts +++ b/src/gConsent/manage/getAccessGrant.test.ts @@ -22,6 +22,8 @@ import type * as CrossFetch from "@inrupt/universal-fetch"; import { Response } from "@inrupt/universal-fetch"; import { beforeAll, describe, expect, it, jest } from "@jest/globals"; +import { isomorphic } from "rdf-isomorphic"; +import { getResources } from "../../common/getters" import { mockAccessGrantObject, @@ -62,34 +64,58 @@ describe("getAccessGrant", () => { expect(mockedFetch).toHaveBeenCalledWith("https://some.credential"); }); - it("throws if resolving the IRI results in an HTTP error", () => { - return expect( + it("throws if resolving the IRI results in an HTTP error", async () => { + await expect( getAccessGrant("https://some.vc.url", { fetch: async () => new Response("Not Found", { status: 404, statusText: "Not Found" }), }), ).rejects.toThrow( - /Could not resolve \[https:\/\/some.vc.url\].*404 Not Found/, + "Fetching the Verifiable Credential [https://some.vc.url] failed: 404 Not Found", + ); + await expect( + getAccessGrant("https://some.vc.url", { + fetch: async () => + new Response("Not Found", { status: 404, statusText: "Not Found" }), + returnLegacyJsonld: false + }), + ).rejects.toThrow( + "Fetching the Verifiable Credential [https://some.vc.url] failed: 404 Not Found", ); }); - it("throws if the given IRI does not resolve to a Verifiable Credential", () => { - return expect( + it("throws if the given IRI does not resolve to a Verifiable Credential", async () => { + await expect( getAccessGrant("https://some.vc.url", { fetch: async () => new Response("{'someKey': 'someValue'}"), }), ).rejects.toThrow( - /Unexpected response.*\[https:\/\/some.vc.url\].*not a Verifiable Credential/, + "Parsing the Verifiable Credential [https://some.vc.url] as JSON failed", + ); + await expect( + getAccessGrant("https://some.vc.url", { + fetch: async () => new Response("{'someKey': 'someValue'}"), + returnLegacyJsonld: false + }), + ).rejects.toThrow( + "Parsing the Verifiable Credential [https://some.vc.url] as JSON failed", ); }); - it("throws if the given IRI does not resolve to a access grant Verifiable Credential", () => { - return expect( + it("throws if the given IRI does not resolve to a access grant Verifiable Credential", async () => { + const fetchFn = async () => + new Response(JSON.stringify(await mockAccessRequestVc()), { + headers: new Headers([["content-type", "application/json"]]), + }) + await expect( getAccessGrant("https://some.vc.url", { - fetch: async () => - new Response(JSON.stringify(await mockAccessRequestVc()), { - headers: new Headers([["content-type", "application/json"]]), - }), + fetch: fetchFn, + }), + ).rejects.toThrow(/not an Access Grant/); + await expect( + getAccessGrant("https://some.vc.url", { + fetch: fetchFn, + returnLegacyJsonld: false }), ).rejects.toThrow(/not an Access Grant/); }); @@ -102,13 +128,20 @@ describe("getAccessGrant", () => { mockedAccessGrant.credentialSubject.providedConsent.hasStatus = "https://w3id.org/GConsent#ConsentStatusDenied"; + const fetchFn = async () => + new Response(JSON.stringify(mockedAccessGrant), { + headers: new Headers([["content-type", "application/json"]]), + }); + const accessGrant = await getAccessGrant("https://some.vc.url", { - fetch: async () => - new Response(JSON.stringify(mockedAccessGrant), { - headers: new Headers([["content-type", "application/json"]]), - }), + fetch: fetchFn, + }); + const accessGrantNoProperties = await getAccessGrant("https://some.vc.url", { + fetch: fetchFn, + returnLegacyJsonld: false }); toBeEqual(accessGrant, mockedAccessGrant); + expect(isomorphic([...accessGrant], [...accessGrantNoProperties])).toBe(true); }); // There is an expect call in the `toBeEqual` function, @@ -129,74 +162,113 @@ describe("getAccessGrant", () => { // eslint-disable-next-line jest/expect-expect it("normalizes equivalent JSON-LD VCs", async () => { const normalizedAccessGrant = mockAccessGrantObject(); - toBeEqual( - await getAccessGrant("https://some.vc.url", { - // The server returns an equivalent JSON-LD with a different frame: - fetch: async () => - new Response( - JSON.stringify({ - ...normalizedAccessGrant, - credentialSubject: { - ...normalizedAccessGrant.credentialSubject, - providedConsent: { - ...normalizedAccessGrant.credentialSubject.providedConsent, - // The 1-value array is replaced by the literal value. - forPersonalData: - normalizedAccessGrant.credentialSubject.providedConsent - .forPersonalData[0], - mode: normalizedAccessGrant.credentialSubject.providedConsent - .mode[0], - inherit: "true", - }, - }, - }), - { - headers: new Headers([["content-type", "application/json"]]), - }, - ), + const fetchFn = async () => + new Response( + JSON.stringify({ + ...normalizedAccessGrant, + credentialSubject: { + ...normalizedAccessGrant.credentialSubject, + providedConsent: { + ...normalizedAccessGrant.credentialSubject.providedConsent, + // The 1-value array is replaced by the literal value. + forPersonalData: + normalizedAccessGrant.credentialSubject.providedConsent + .forPersonalData[0], + mode: normalizedAccessGrant.credentialSubject.providedConsent + .mode[0], + inherit: "true", + }, + }, }), + { + headers: new Headers([["content-type", "application/json"]]), + }, + ); + + const accessGrant = await getAccessGrant("https://some.vc.url", { + // The server returns an equivalent JSON-LD with a different frame: + fetch: fetchFn, + }); + toBeEqual( + accessGrant, mockAccessGrant, ); + expect(accessGrant.credentialSubject.providedConsent.forPersonalData).toEqual(["https://some.resource"]); + expect(getResources(accessGrant)).toEqual(["https://some.resource"]); + + const accessGrantNoProperties = await getAccessGrant(new URL("https://some.vc.url"), { + fetch: fetchFn, + returnLegacyJsonld: false + }); + // @ts-expect-error no object properties should be available + expect(accessGrantNoProperties.credentialSubject).toBeUndefined(); + expect(getResources(accessGrantNoProperties)).toEqual(["https://some.resource"]); + + expect(isomorphic([...accessGrant], [...accessGrantNoProperties])).toBe(true); }); // There is an expect call in the `toBeEqual` function, // but the linter doesn't pick up on this. // eslint-disable-next-line jest/expect-expect it("returns the access grant with the given URL object", async () => { - const mockedFetch = jest.fn(global.fetch).mockResolvedValueOnce( - new Response(JSON.stringify(mockAccessGrantObject()), { - headers: new Headers([["content-type", "application/json"]]), - }), - ); + const mockedFetch = jest.fn(async () => new Response(JSON.stringify(mockAccessGrantObject()), { + headers: new Headers([["content-type", "application/json"]]), + })); const accessGrant = await getAccessGrant(new URL("https://some.vc.url"), { fetch: mockedFetch, }); toBeEqual(accessGrant, mockAccessGrant); + expect(accessGrant.credentialSubject.providedConsent.forPersonalData).toEqual(["https://some.resource"]); + expect(getResources(accessGrant)).toEqual(["https://some.resource"]); + + const accessGrantNoProperties = await getAccessGrant(new URL("https://some.vc.url"), { + fetch: mockedFetch, + returnLegacyJsonld: false + }); + // @ts-expect-error no object properties should be available + expect(accessGrantNoProperties.credentialSubject).toBeUndefined(); + expect(getResources(accessGrantNoProperties)).toEqual(["https://some.resource"]); + + expect(isomorphic([...accessGrant], [...accessGrantNoProperties])).toBe(true); }); - it("errors if the response is not a full access grant", () => { - return expect( + it("errors if the response is not a full access grant", async () => { + const fetchFn = async () => + new Response( + JSON.stringify({ + "@context": "https://www.w3.org/2018/credentials/v1", + id: "https://some.credential", + }), + ) + await expect( getAccessGrant(new URL("https://some.vc.url"), { - fetch: async () => - new Response( - JSON.stringify({ - "@context": "https://www.w3.org/2018/credentials/v1", - id: "https://some.credential", - }), - ), + fetch: fetchFn, }), - ).rejects.toThrow("the result is not a Verifiable Credential"); + ).rejects.toThrow("The value received from [https://some.vc.url/] is not a Verifiable Credential"); + await expect( + getAccessGrant(new URL("https://some.vc.url"), { + fetch: fetchFn, + returnLegacyJsonld: false + }), + ).rejects.toThrow("The value received from [https://some.vc.url/] is not a Verifiable Credential"); }); - it("errors if the response is an empty json object", () => { - return expect( + it("errors if the response is an empty json object", async () => { + const fetchFn = async () => + new Response(JSON.stringify({}), { + headers: new Headers([["content-type", "application/json"]]), + }); + await expect( getAccessGrant(new URL("https://some.vc.url"), { - fetch: async () => - new Response(JSON.stringify({}), { - headers: new Headers([["content-type", "application/json"]]), - }), + fetch: fetchFn, + }), + ).rejects.toThrow("The value received from [https://some.vc.url/] is not a Verifiable Credential"); + await expect( + getAccessGrant(new URL("https://some.vc.url"), { + fetch: fetchFn, + returnLegacyJsonld: false }), - ).rejects.toThrow("the result is not a Verifiable Credential"); + ).rejects.toThrow("Verifiable credential is not an object, or does not have an id"); }); }); diff --git a/src/gConsent/manage/getAccessGrant.ts b/src/gConsent/manage/getAccessGrant.ts index 0bb351a21..c87ca4da9 100644 --- a/src/gConsent/manage/getAccessGrant.ts +++ b/src/gConsent/manage/getAccessGrant.ts @@ -20,17 +20,15 @@ // import type { UrlString } from "@inrupt/solid-client"; -import type { VerifiableCredential } from "@inrupt/solid-client-vc"; -import { - isVerifiableCredential, - verifiableCredentialToDataset, -} from "@inrupt/solid-client-vc"; -import { getSessionFetch } from "../../common/util/getSessionFetch"; +import { DatasetWithId, getVerifiableCredential } from "@inrupt/solid-client-vc"; +import { DataFactory } from "n3"; +import { isGConsentAccessGrant } from "../../common/getters"; import { isAccessGrant } from "../guard/isAccessGrant"; -import { isBaseAccessGrantVerifiableCredential } from "../guard/isBaseAccessGrantVerifiableCredential"; +import { isBaseAccessGrantVerifiableCredential, isRdfjsBaseAccessGrantVerifiableCredential } from "../guard/isBaseAccessGrantVerifiableCredential"; import type { AccessBaseOptions } from "../type/AccessBaseOptions"; import type { AccessGrant } from "../type/AccessGrant"; import { normalizeAccessGrant } from "./approveAccessRequest"; +const { namedNode } = DataFactory; /** * Retrieve the Access Grant associated to the given URL. @@ -39,41 +37,69 @@ import { normalizeAccessGrant } from "./approveAccessRequest"; * @param options Optional properties to customise the request behaviour. * @returns The Verifiable Credential associated to the given IRI, if it is an access grant. Throws otherwise. * @since 0.4.0 + * @deprecated set returnLegacyJsonld: false and use RDFJS API */ export async function getAccessGrant( accessGrantVcUrl: UrlString | URL, - options?: AccessBaseOptions, -): Promise { - const sessionFetch = await getSessionFetch(options ?? {}); + options?: AccessBaseOptions & { + returnLegacyJsonld?: true; + }, +): Promise +/** + * Retrieve the Access Grant associated to the given URL. + * + * @param accessGrantVcUrl The URL of an access grant, with or without consent. + * @param options Optional properties to customise the request behaviour. + * @returns The Verifiable Credential associated to the given IRI, if it is an access grant. Throws otherwise. + * @since 0.4.0 + */ +export async function getAccessGrant( + accessGrantVcUrl: UrlString | URL, + options?: AccessBaseOptions & { + returnLegacyJsonld?: boolean; + }, +): Promise +/** + * Retrieve the Access Grant associated to the given URL. + * + * @param accessGrantVcUrl The URL of an access grant, with or without consent. + * @param options Optional properties to customise the request behaviour. + * @returns The Verifiable Credential associated to the given IRI, if it is an access grant. Throws otherwise. + * @since 0.4.0 + */ +export async function getAccessGrant( + accessGrantVcUrl: UrlString | URL, + options?: AccessBaseOptions & { + returnLegacyJsonld?: boolean; + }, +): Promise { const vcUrl = typeof accessGrantVcUrl === "string" ? accessGrantVcUrl : accessGrantVcUrl.href; - const response = await sessionFetch(vcUrl); - if (!response.ok) { - throw new Error( - `Could not resolve [${vcUrl}]: ${response.status} ${response.statusText}`, - ); - } - const responseErrorClone = await response.text(); - let data; - try { - data = await verifiableCredentialToDataset( - normalizeAccessGrant(JSON.parse(responseErrorClone)), - { - baseIRI: accessGrantVcUrl.toString(), - includeVcProperties: true, - }, - ); - } catch (e) { - throw new Error( - `Unexpected response when resolving [${vcUrl}], the result is not a Verifiable Credential: ${responseErrorClone}.\n\nError details: ${e}`, - ); + + if (options?.returnLegacyJsonld === false) { + const data = await getVerifiableCredential(vcUrl, { + fetch: options?.fetch, + returnLegacyJsonld: false + }); + + if (!isRdfjsBaseAccessGrantVerifiableCredential(data) || !isGConsentAccessGrant(data)) { + throw new Error( + `Unexpected response when resolving [${vcUrl}], the result is not an Access Grant: ${JSON.stringify( + data, null, 2 + )}`, + ); + } + return data; } + + const data = await getVerifiableCredential(vcUrl, { + fetch: options?.fetch, + normalize: normalizeAccessGrant + }); if ( - !isVerifiableCredential(data) || - !isBaseAccessGrantVerifiableCredential(data) || - !isAccessGrant(data) + !isBaseAccessGrantVerifiableCredential(data) || !isAccessGrant(data) ) { throw new Error( `Unexpected response when resolving [${vcUrl}], the result is not an Access Grant: ${JSON.stringify( diff --git a/src/gConsent/request/issueAccessRequest.ts b/src/gConsent/request/issueAccessRequest.ts index ca58d83c9..b1df67f44 100644 --- a/src/gConsent/request/issueAccessRequest.ts +++ b/src/gConsent/request/issueAccessRequest.ts @@ -19,7 +19,7 @@ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import type { VerifiableCredential } from "@inrupt/solid-client-vc"; +import type { VerifiableCredential, VerifiableCredentialBase } from "@inrupt/solid-client-vc"; import { getRequestBody, issueAccessVc } from "../util/issueAccessVc"; import type { AccessBaseOptions } from "../type/AccessBaseOptions"; import type { @@ -41,7 +41,7 @@ import { gc } from "../../common/constants"; * @param accessRequest The grant returned by the VC issuer * @returns An equivalent JSON-LD document framed according to our typing. */ -export function normalizeAccessRequest( +export function normalizeAccessRequest( accessRequest: T, ): T { // Proper type checking is performed after normalization, so casting here is fine.