From 07c4461f70443025021aa72bbadfe2a5f163feb7 Mon Sep 17 00:00:00 2001 From: Kyle Huang Junyuan Date: Fri, 26 Jul 2024 14:22:38 +0800 Subject: [PATCH] feat: prefetch frequently used contexts in OA v4 --- package.json | 1 + scripts/fetchV4Contexts.ts | 25 ++ src/4.0/context.ts | 36 ++- ...n.com-com-openattestation-4.0-context.json | 63 ++++ .../https---www.w3.org-ns-credentials-v2 | 301 ++++++++++++++++++ 5 files changed, 416 insertions(+), 10 deletions(-) create mode 100644 scripts/fetchV4Contexts.ts create mode 100644 src/4.0/contexts/__generated__/https---schemata.openattestation.com-com-openattestation-4.0-context.json create mode 100644 src/4.0/contexts/__generated__/https---www.w3.org-ns-credentials-v2 diff --git a/package.json b/package.json index 5fedc9ef..e6b8d760 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "test:vc": "scripts/runVcTest.sh", "lint": "eslint . --ext .ts,.json --max-warnings 0", "lint:fix": "npm run lint -- --fix", + "fetch-v4-contexts": "npx ts-node scripts/fetchV4Contexts.ts", "generate-v4-fixtures": "npx ts-node scripts/generateV4JsonFixtures.ts", "generate-v4-json-schemas": "npx ts-node scripts/generateV4JsonSchemas.ts", "publish:schema": "./scripts/publishSchema.sh", diff --git a/scripts/fetchV4Contexts.ts b/scripts/fetchV4Contexts.ts new file mode 100644 index 00000000..9dd9ceed --- /dev/null +++ b/scripts/fetchV4Contexts.ts @@ -0,0 +1,25 @@ +import fs from "fs"; +import path from "path"; +import { ContextUrl, urlToSafeFilename } from "../src/4.0/context"; + +const OUTPUT_DIR = path.resolve("./src/4.0/contexts/__generated__"); + +// make sure the output directory exists +if (fs.existsSync(OUTPUT_DIR)) { + fs.rmSync(OUTPUT_DIR, { recursive: true }); +} +fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + +const CONTEXTS_TO_FETCH = Object.values(ContextUrl); + +for (const url of CONTEXTS_TO_FETCH) { + fetch(url) + .then((res) => res.json()) + .then((context) => { + const filename = urlToSafeFilename(url); + fs.writeFileSync(path.join(OUTPUT_DIR, filename), JSON.stringify(context, null, 2)); + }) + .catch((err) => { + console.error(`Unable to fetch OA v4.0 context`, err); + }); +} diff --git a/src/4.0/context.ts b/src/4.0/context.ts index b7ce5582..417967c2 100644 --- a/src/4.0/context.ts +++ b/src/4.0/context.ts @@ -1,5 +1,6 @@ -import { expand, Options, JsonLdDocument } from "jsonld"; import { fetch } from "cross-fetch"; +import { readFile } from "fs/promises"; +import { expand, Options, JsonLdDocument } from "jsonld"; export const ContextUrl = { w3c_vc_v2: "https://www.w3.org/ns/credentials/v2", @@ -11,25 +12,31 @@ export const ContextType = { OAV4Context: "OpenAttestationCredential", } as const; -const preloadedContextList = [ContextUrl.w3c_vc_v2, ContextUrl.oa_vc_v4]; -const contexts: Map = new Map(); +const PREFETECHED_CONTEXT_LIST = Object.values(ContextUrl); +const contextCache: Map = new Map(); -// Preload frequently used contexts -// https://github.com/digitalbazaar/jsonld.js?tab=readme-ov-file#custom-document-loader let isFirstLoad = true; +// https://github.com/digitalbazaar/jsonld.js?tab=readme-ov-file#custom-document-loader // FIXME: @types/json-ld seems to be outdated as callback is supposed to be options const documentLoader: Options.DocLoader["documentLoader"] = async (url, _) => { + // On first load: Preload frequently used contexts from "src/4.0/contexts/__generated__/*" if (isFirstLoad) { isFirstLoad = false; - for (const url of preloadedContextList) { - const document = await fetch(url).then((res) => res.json()); - contexts.set(url, document); + for (const url of PREFETECHED_CONTEXT_LIST) { + try { + const filename = urlToSafeFilename(url); + const document = await readFile(`../4.0/contexts/__generated__/${filename}`, "utf-8"); + const parsed = JSON.parse(document); + contextCache.set(url, parsed); + } catch (e) { + console.warn(`Unable to prefetch context from ${url}`, e); + } } } - if (contexts.get(url)) { + if (contextCache.get(url)) { return { contextUrl: undefined, // this is for a context via a link header - document: contexts.get(url), // this is the actual document that was loaded + document: contextCache.get(url), // this is the actual document that was loaded documentUrl: url, // this is the actual context URL after redirects }; } else { @@ -57,3 +64,12 @@ export class UnableToInterpretContextError extends Error { Object.setPrototypeOf(this, UnableToInterpretContextError.prototype); } } + +/** + * Convert URL to a filename-safe string + * @param url string + * @returns string + */ +export function urlToSafeFilename(url: string) { + return url.replace(/[/\\?%*:|"<>]/g, "-"); +} diff --git a/src/4.0/contexts/__generated__/https---schemata.openattestation.com-com-openattestation-4.0-context.json b/src/4.0/contexts/__generated__/https---schemata.openattestation.com-com-openattestation-4.0-context.json new file mode 100644 index 00000000..ea129970 --- /dev/null +++ b/src/4.0/contexts/__generated__/https---schemata.openattestation.com-com-openattestation-4.0-context.json @@ -0,0 +1,63 @@ +{ + "@context": { + "@version": 1.1, + "@protected": true, + "schema": "https://schema.org/", + "oacred": "https://schemata.openattestation.com/com/openattestation/4.0/context.json#", + "OpenAttestationCredential": { + "@id": "oacred:OpenAttestationCredential", + "@context": { + "name": "schema:name" + } + }, + "OpenAttestationIssuer": { + "@id": "oacred:OpenAttestationIssuer", + "@context": { + "name": "schema:name", + "identityProof": { + "@id": "oacred:identityProof", + "@context": { + "identityProofType": "oacred:identityProofType", + "identifier": { + "@id": "oacred:identityProofIdentifier", + "@type": "schema:identifier" + } + } + } + } + }, + "OpenAttestationOcspResponder": { + "@id": "oacred:credentialStatus" + }, + "OpenAttestationEmbeddedRenderer": { + "@id": "oacred:renderMethod", + "@context": { + "templateName": "oacred:renderTemplateName" + } + }, + "OpenAttestationMerkleProofSignature2018": { + "@id": "oacred:OpenAttestationMerkleProofSignature2018", + "@context": { + "proofPurpose": "oacred:proofPurpose", + "targetHash": "oacred:proofTargetHash", + "proofs": { + "@id": "oacred:proofIntermediateProofs", + "@container": "@list" + }, + "merkleRoot": "oacred:proofMerkleRoot", + "salts": "oacred:proofSalts", + "privacy": { + "@id": "oacred:proofPrivacy", + "@context": { + "obfuscated": { + "@id": "oacred:proofPrivacyObfuscated", + "@container": "@list" + } + } + }, + "key": "oacred:proofKey", + "signature": "oacred:proofSignature" + } + } + } +} \ No newline at end of file diff --git a/src/4.0/contexts/__generated__/https---www.w3.org-ns-credentials-v2 b/src/4.0/contexts/__generated__/https---www.w3.org-ns-credentials-v2 new file mode 100644 index 00000000..b8c91de0 --- /dev/null +++ b/src/4.0/contexts/__generated__/https---www.w3.org-ns-credentials-v2 @@ -0,0 +1,301 @@ +{ + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "description": "https://schema.org/description", + "digestMultibase": { + "@id": "https://w3id.org/security#digestMultibase", + "@type": "https://w3id.org/security#multibase" + }, + "digestSRI": { + "@id": "https://www.w3.org/2018/credentials#digestSRI", + "@type": "https://www.w3.org/2018/credentials#sriString" + }, + "mediaType": { + "@id": "https://schema.org/encodingFormat" + }, + "name": "https://schema.org/name", + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "confidenceMethod": { + "@id": "https://www.w3.org/2018/credentials#confidenceMethod", + "@type": "@id" + }, + "credentialSchema": { + "@id": "https://www.w3.org/2018/credentials#credentialSchema", + "@type": "@id" + }, + "credentialStatus": { + "@id": "https://www.w3.org/2018/credentials#credentialStatus", + "@type": "@id" + }, + "credentialSubject": { + "@id": "https://www.w3.org/2018/credentials#credentialSubject", + "@type": "@id" + }, + "description": "https://schema.org/description", + "evidence": { + "@id": "https://www.w3.org/2018/credentials#evidence", + "@type": "@id" + }, + "issuer": { + "@id": "https://www.w3.org/2018/credentials#issuer", + "@type": "@id" + }, + "name": "https://schema.org/name", + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + }, + "refreshService": { + "@id": "https://www.w3.org/2018/credentials#refreshService", + "@type": "@id" + }, + "relatedResource": { + "@id": "https://www.w3.org/2018/credentials#relatedResource", + "@type": "@id" + }, + "renderMethod": { + "@id": "https://www.w3.org/2018/credentials#renderMethod", + "@type": "@id" + }, + "termsOfUse": { + "@id": "https://www.w3.org/2018/credentials#termsOfUse", + "@type": "@id" + }, + "validFrom": { + "@id": "https://www.w3.org/2018/credentials#validFrom", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "validUntil": { + "@id": "https://www.w3.org/2018/credentials#validUntil", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + } + } + }, + "EnvelopedVerifiableCredential": "https://www.w3.org/2018/credentials#EnvelopedVerifiableCredential", + "VerifiablePresentation": { + "@id": "https://www.w3.org/2018/credentials#VerifiablePresentation", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "holder": { + "@id": "https://www.w3.org/2018/credentials#holder", + "@type": "@id" + }, + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + }, + "termsOfUse": { + "@id": "https://www.w3.org/2018/credentials#termsOfUse", + "@type": "@id" + }, + "verifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#verifiableCredential", + "@type": "@id", + "@container": "@graph", + "@context": null + } + } + }, + "EnvelopedVerifiablePresentation": "https://www.w3.org/2018/credentials#EnvelopedVerifiablePresentation", + "JsonSchemaCredential": "https://www.w3.org/2018/credentials#JsonSchemaCredential", + "JsonSchema": { + "@id": "https://www.w3.org/2018/credentials#JsonSchema", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "jsonSchema": { + "@id": "https://www.w3.org/2018/credentials#jsonSchema", + "@type": "@json" + } + } + }, + "BitstringStatusListCredential": "https://www.w3.org/ns/credentials/status#BitstringStatusListCredential", + "BitstringStatusList": { + "@id": "https://www.w3.org/ns/credentials/status#BitstringStatusList", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "encodedList": { + "@id": "https://www.w3.org/ns/credentials/status#encodedList", + "@type": "https://w3id.org/security#multibase" + }, + "statusMessage": { + "@id": "https://www.w3.org/ns/credentials/status#statusMessage", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "message": "https://www.w3.org/ns/credentials/status#message", + "status": "https://www.w3.org/ns/credentials/status#status" + } + }, + "statusPurpose": "https://www.w3.org/ns/credentials/status#statusPurpose", + "statusReference": { + "@id": "https://www.w3.org/ns/credentials/status#statusReference", + "@type": "@id" + }, + "statusSize": { + "@id": "https://www.w3.org/ns/credentials/status#statusSize", + "@type": "https://www.w3.org/2001/XMLSchema#positiveInteger" + }, + "ttl": "https://www.w3.org/ns/credentials/status#ttl" + } + }, + "BitstringStatusListEntry": { + "@id": "https://www.w3.org/ns/credentials/status#BitstringStatusListEntry", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "statusListCredential": { + "@id": "https://www.w3.org/ns/credentials/status#statusListCredential", + "@type": "@id" + }, + "statusListIndex": "https://www.w3.org/ns/credentials/status#statusListIndex", + "statusPurpose": "https://www.w3.org/ns/credentials/status#statusPurpose" + } + }, + "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" + }, + "cryptosuite": { + "@id": "https://w3id.org/security#cryptosuite", + "@type": "https://w3id.org/security#cryptosuiteString" + }, + "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", + "previousProof": { + "@id": "https://w3id.org/security#previousProof", + "@type": "@id" + }, + "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" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@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" + } + } + }, + "...": { + "@id": "https://www.iana.org/assignments/jwt#..." + }, + "_sd": { + "@id": "https://www.iana.org/assignments/jwt#_sd", + "@type": "@json" + }, + "_sd_alg": { + "@id": "https://www.iana.org/assignments/jwt#_sd_alg" + }, + "aud": { + "@id": "https://www.iana.org/assignments/jwt#aud", + "@type": "@id" + }, + "cnf": { + "@id": "https://www.iana.org/assignments/jwt#cnf", + "@context": { + "@protected": true, + "kid": { + "@id": "https://www.iana.org/assignments/jwt#kid", + "@type": "@id" + }, + "jwk": { + "@id": "https://www.iana.org/assignments/jwt#jwk", + "@type": "@json" + } + } + }, + "exp": { + "@id": "https://www.iana.org/assignments/jwt#exp", + "@type": "https://www.w3.org/2001/XMLSchema#nonNegativeInteger" + }, + "iat": { + "@id": "https://www.iana.org/assignments/jwt#iat", + "@type": "https://www.w3.org/2001/XMLSchema#nonNegativeInteger" + }, + "iss": { + "@id": "https://www.iana.org/assignments/jose#iss", + "@type": "@id" + }, + "jku": { + "@id": "https://www.iana.org/assignments/jose#jku", + "@type": "@id" + }, + "kid": { + "@id": "https://www.iana.org/assignments/jose#kid", + "@type": "@id" + }, + "nbf": { + "@id": "https://www.iana.org/assignments/jwt#nbf", + "@type": "https://www.w3.org/2001/XMLSchema#nonNegativeInteger" + }, + "sub": { + "@id": "https://www.iana.org/assignments/jose#sub", + "@type": "@id" + }, + "x5u": { + "@id": "https://www.iana.org/assignments/jose#x5u", + "@type": "@id" + } + } +} \ No newline at end of file