From d3aafc2a165879a71a4d48838a1c86954f1ce073 Mon Sep 17 00:00:00 2001 From: Kyle Huang Junyuan Date: Wed, 24 Apr 2024 16:20:00 +0800 Subject: [PATCH 01/43] wip: oa v4 types --- scripts/postInstall.js | 5 - scripts/publishSchema.sh | 4 - src/4.0/schema/schema.json | 183 ---------------------------------- src/4.0/schema/schema.test.ts | 36 ------- src/4.0/sign.ts | 2 +- src/4.0/types.ts | 136 ++++++++++++++++--------- src/4.0/validate/dataModel.ts | 4 +- src/4.0/wrap.ts | 58 +++++++---- src/index.ts | 26 ++--- src/shared/@types/document.ts | 14 ++- src/shared/@types/wrap.ts | 4 - src/shared/utils/guard.ts | 5 +- src/shared/utils/utils.ts | 21 +++- 13 files changed, 171 insertions(+), 327 deletions(-) delete mode 100644 src/4.0/schema/schema.json delete mode 100644 src/4.0/schema/schema.test.ts diff --git a/scripts/postInstall.js b/scripts/postInstall.js index d09dd7d3..3d561818 100644 --- a/scripts/postInstall.js +++ b/scripts/postInstall.js @@ -16,11 +16,6 @@ if (fs.existsSync(quicktype) && process.env.npm_config_production !== "true") { quicktype + " -s schema -o src/__generated__/schema.3.0.ts -t OpenAttestationDocument --just-types src/3.0/schema/schema.json --no-date-times" ); - console.log('"Creating types from src/4.0/schema/schema.json"'); - execSync( - quicktype + - " -s schema -o src/__generated__/schema.4.0.ts -t OpenAttestationDocument --just-types src/4.0/schema/schema.json --no-date-times" - ); } else { console.log("Not running quicktype"); } diff --git a/scripts/publishSchema.sh b/scripts/publishSchema.sh index 6064f6c4..41680371 100755 --- a/scripts/publishSchema.sh +++ b/scripts/publishSchema.sh @@ -11,7 +11,3 @@ cp src/2.0/schema/schema.json public/2.0/schema.json # Copy 3.0 schema to public folder mkdir -p public/3.0/ cp src/3.0/schema/schema.json public/3.0/schema.json - -# Copy 4.0 schema to public folder -mkdir -p public/4.0/ -cp src/4.0/schema/schema.json public/4.0/schema.json diff --git a/src/4.0/schema/schema.json b/src/4.0/schema/schema.json deleted file mode 100644 index aca24d0e..00000000 --- a/src/4.0/schema/schema.json +++ /dev/null @@ -1,183 +0,0 @@ -{ - "title": "OpenAttestation v4.0 Schema", - "$id": "https://schemata.openattestation.com/com/openattestation/4.0/alpha-schema.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "definitions": { - "@context": { - "description": "Used to define the short-hand names that are used throughout a JSON-LD document.", - "type": ["object", "string", "array", "null"] - }, - "id": { - "description": "Used to uniquely identify things that are being described in the document with IRIs or blank node identifiers.", - "type": "string", - "format": "uri" - }, - "type": { - "description": "Used to set the data type of a node or typed value.", - "type": ["string", "null", "array"] - }, - "credentialSchema": { - "type": "object", - "properties": { - "id": { "$ref": "#/definitions/id" }, - "type": { "$ref": "#/definitions/type" } - }, - "description": "A data schema that provide verifiers with enough information to determine whether the provided data conforms to the provided schema(s). More information in https://www.w3.org/TR/vc-data-model-2.0/#data-schemas" - }, - "renderMethod": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uri", - "description": "URL of a decentralised renderer to render this document", - "examples": ["https://demo-renderer.openattestation.com"] - }, - "type": { "$ref": "#/definitions/type", "examples": ["OpenAttestationEmbeddedRenderer"] }, - "templateName": { - "type": "string", - "description": "Template name to be use by template renderer to determine the template to use" - } - }, - "required": ["id", "templateName"] - }, - "credentialSubject": { - "type": "object", - "properties": { - "id": { "$ref": "#/definitions/id" }, - "type": { "$ref": "#/definitions/type" } - }, - "description": "A verifiable credential contains claims about one or more subjects. More information in https://www.w3.org/TR/vc-data-model-2.0/#credential-subject" - }, - "termsOfUse": { - "type": "object", - "properties": { - "type": { "$ref": "#/definitions/type" } - } - }, - "evidence": { - "type": "object", - "properties": { - "type": { "$ref": "#/definitions/type" } - } - }, - "proof": { - "type": "object", - "properties": { - "type": { "$ref": "#/definitions/type", "examples": ["OpenAttestationMerkleProofSignature2018"] }, - "proofPurpose": { "type": "string", "enum": ["assertionMethod"] }, - "targetHash": { "type": "string" }, - "proofs": { "type": "array", "items": { "type": "string" } }, - "merkleRoot": { "type": "string" }, - "salts": { "type": "string" }, - "privacy": { - "type": "object", - "properties": { "obfuscated": { "type": "array", "items": { "type": "string" } } } - }, - "key": { "type": "string" }, - "signature": { "type": "string" } - }, - "additionalProperties": false - } - }, - "properties": { - "@context": { "$ref": "#/definitions/@context" }, - "id": { "$ref": "#/definitions/id" }, - "type": { "$ref": "#/definitions/type", "examples": ["VerifiableCredential", "OpenAttestationCredential"] }, - "credentialSchema": { - "anyOf": [ - { "$ref": "#/definitions/credentialSchema" }, - { "type": "array", "items": { "$ref": "#/definitions/credentialSchema" } } - ] - }, - "validFrom": { - "type": "string", - "format": "date-time", - "description": "The date and time when this credential becomes valid", - "examples": ["2024-03-08T12:00:00+08:00"] - }, - "validUntil": { - "type": "string", - "format": "date-time", - "description": "The date and time when this credential expires", - "examples": ["2024-03-27T12:00:00+08:00"] - }, - "name": { - "type": "string", - "description": "Human readable name of this credential" - }, - "issuer": { - "oneOf": [ - { "type": "string" }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uri", - "description": "URI when dereferenced, results in a document containing machine-readable information about the issuer that can be used to verify the information expressed in the credential. More information in https://www.w3.org/TR/vc-data-model/#issuer" - }, - "type": { "$ref": "#/definitions/type", "examples": ["OpenAttestationIssuer"] }, - "name": { - "type": "string", - "description": "Issuer's name" - }, - "identityProof": { - "type": "object", - "properties": { - "identityProofType": { - "type": "string", - "enum": ["DNS-TXT", "DNS-DID", "DID"] - }, - "identifier": { - "type": "string", - "description": "Identifier to be shown to end user upon verifying the identity" - } - }, - "required": ["identityProofType", "identifier"], - "additionalProperties": false - } - }, - "required": ["id"] - } - ] - }, - "credentialStatus": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uri", - "description": "URI to the status of the credential as explained by https://www.w3.org/TR/vc-data-model/#status", - "examples": ["https://ocsp-sandbox.openattestation.com"] - }, - "type": { "$ref": "#/definitions/type", "examples": ["OpenAttestationOcspResponder"] } - }, - "additionalProperties": false - }, - "renderMethod": { "type": "array", "items": { "$ref": "#/definitions/renderMethod" } }, - "credentialSubject": { - "anyOf": [ - { "$ref": "#/definitions/credentialSubject" }, - { "type": "array", "items": { "$ref": "#/definitions/credentialSubject" } } - ] - }, - "termsOfUse": { - "anyOf": [ - { "$ref": "#/definitions/termsOfUse" }, - { "type": "array", "items": { "$ref": "#/definitions/termsOfUse" } } - ] - }, - "evidence": { - "anyOf": [ - { "$ref": "#/definitions/evidence" }, - { "type": "array", "items": { "$ref": "#/definitions/evidence" } } - ] - }, - "proof": { - "anyOf": [{ "$ref": "#/definitions/proof" }, { "type": "array", "items": { "$ref": "#/definitions/proof" } }] - } - }, - "required": ["@context", "type", "issuer", "credentialSubject"] -} diff --git a/src/4.0/schema/schema.test.ts b/src/4.0/schema/schema.test.ts deleted file mode 100644 index ee1960b5..00000000 --- a/src/4.0/schema/schema.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-disable jest/no-try-expect,jest/no-conditional-expect */ -import { cloneDeep } from "lodash"; -import { _unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocument as wrapDocumentV4 } from "../../index"; -import sample from "../../../test/fixtures/v4/did-raw.json"; -import { ContextUrl } from "../../shared/@types/document"; -import { OpenAttestationDocument } from "../../__generated__/schema.4.0"; - -const sampleVc = sample as OpenAttestationDocument; - -// eslint-disable-next-line jest/no-disabled-tests -describe("schema/4.0", () => { - it("should be valid with sample document", async () => { - const document = cloneDeep(sampleVc); - const wrappedDocument = await wrapDocumentV4(document); - expect(wrappedDocument["type"]).toStrictEqual(["VerifiableCredential", "OpenAttestationCredential"]); - expect(wrappedDocument["proof"]["type"]).toStrictEqual("OpenAttestationMerkleProofSignature2018"); - }); - - it("should be valid when adding any additional data", async () => { - const document = { ...cloneDeep(sampleVc), key1: "some" }; - const wrappedDocument = await wrapDocumentV4(document); - expect(wrappedDocument["key1"]).toStrictEqual("some"); - expect(wrappedDocument["type"]).toStrictEqual(["VerifiableCredential", "OpenAttestationCredential"]); - expect(wrappedDocument["proof"]["type"]).toStrictEqual("OpenAttestationMerkleProofSignature2018"); - }); - - describe("@context", () => { - it("should be invalid if @context contains one invalid URI", async () => { - expect.assertions(1); - const document = { ...cloneDeep(sampleVc), "@context": [ContextUrl.v2_vc, "bad string"] }; - await expect(wrapDocumentV4(document)).rejects.toMatchInlineSnapshot( - `[Error: Unable to interpret @context: {"name":"jsonld.InvalidUrl","details":{"code":"loading remote context failed","url":"bad string","cause":{}}}]` - ); - }); - }); -}); diff --git a/src/4.0/sign.ts b/src/4.0/sign.ts index 0519604e..7ba89797 100644 --- a/src/4.0/sign.ts +++ b/src/4.0/sign.ts @@ -1,4 +1,4 @@ -import { OpenAttestationDocument, WrappedDocument, SignedWrappedDocument, SignedWrappedProof } from "./types"; +import { OpenAttestationVC, WrappedDocument, SignedWrappedDocument, SignedWrappedProof } from "./types"; import { sign } from "../shared/signer"; import { SigningKey, SUPPORTED_SIGNING_ALGORITHM } from "../shared/@types/sign"; import { isSignedWrappedV4Document } from "../shared/utils"; diff --git a/src/4.0/types.ts b/src/4.0/types.ts index 424fb973..d4e67410 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -1,55 +1,101 @@ -// types generated by quicktype during postinstall phase -import { OpenAttestationDocument as OpenAttestationDocumentV4, ProofPurpose } from "../__generated__/schema.4.0"; -import { OpenAttestationHexString, SignatureAlgorithm } from "../shared/@types/document"; -import { Array, Literal, Record, Static, String, Union } from "runtypes"; - -export interface Salt { - value: string; - path: string; -} - -export const PrivacyObfuscation = Record({ - obfuscated: Array(OpenAttestationHexString), -}); -export type PrivacyObfuscation = Static; - -export const WrappedProof = Record({ - type: SignatureAlgorithm, - /* FIXME: No straightforward way to represent enum in runtypes */ - // proofPurpose: runtypesFromEnum(ProofPurpose), - // proofPurpose: ProofPurpose, - proofPurpose: Union(Literal(ProofPurpose.AssertionMethod)), - targetHash: String, - proofs: Array(String), - merkleRoot: String, - salts: String, - privacy: PrivacyObfuscation, +import z from "zod"; + +// import { OpenAttestationDocument as OpenAttestationDocumentV4, ProofPurpose } from "../__generated__/schema.4.0"; +import { vcDataModel, zodUri } from "./validate/dataModel"; +import { ContextUrl, ContextType, OpenAttestationHexString, SignatureAlgorithm } from "../shared/@types/document"; + +const IdentityProofType = z.enum(["DNS-TXT", "DNS-DID", "DID"]); +type IdentityProofType = z.infer; + +const Salt = z.object({ value: z.string(), path: z.string() }); +type Salt = z.infer; + +// Custom hex string validation function +const HEX_STRING_REGEX = /^(0x)?[0-9a-fA-F]{40}$/; +const zodHexString = z.string().regex(HEX_STRING_REGEX, { message: "Invalid Hex String" }); + +const OpenAttestationVC = vcDataModel.extend({ + "@context": z + // Must be an array that starts with [baseContext, v4Context, ...] + .tuple([z.literal(ContextUrl.v2_vc), z.literal(ContextUrl.v4_alpha)]) + // Remaining items can be string or object + .rest(z.union([z.string(), z.record(z.any())])), + + type: z + // Must be an array that starts with [VerifiableCredential, OpenAttestationCredential, ...] + .tuple([z.literal(ContextType.BaseContext as string), z.literal(ContextType.V4AlphaContext as string)]) + // Remaining items can be string + .rest(z.string()), + + issuer: z.object({ + // Must have id match uri pattern + id: zodUri, + type: z.literal("OpenAttestationIssuer"), + name: z.string(), + identityProof: z.object({ + identityProofType: IdentityProofType, + identifier: z.string(), + }), + }), + + // [Optional] OCSP Revocation + credentialStatus: z + .object({ + // Must have id match url pattern (OCSP endpoint) + id: z.string().url(), + type: z.literal("OpenAttestationOcspResponder"), + }) + .optional(), + + // [Optional] Render Method + renderMethod: z + .array( + z.discriminatedUnion("type", [ + /* OA Decentralised Embedded Renderer */ + z.object({ + // Must have id match url pattern + id: z.string().url(), + type: z.literal("OpenAttestationEmbeddedRenderer"), + templateName: z.string(), + }), + /* SVG Renderer (URL or Embedded) */ + z.object({ + // Must have id match url pattern or embeded SVG string + id: z.union([z.string(), z.string().url()]), + type: z.literal("SvgRenderingTemplate2023"), + name: z.string(), + digestMultibase: z.string(), + }), + ]) + ) + .optional(), }); -export type WrappedProof = Static; +type VC = z.infer; +type OpenAttestationVC = z.infer; -export const WrappedProofStrict = WrappedProof.And( - Record({ - targetHash: OpenAttestationHexString, - merkleRoot: OpenAttestationHexString, - proofs: Array(OpenAttestationHexString), - }) -); -export type WrappedProofStrict = Static; +const WrappedProof = z.object({ + type: z.literal("OpenAttestationMerkleProofSignature2018"), + proofPurpose: z.literal("assertionMethod"), + targetHash: zodHexString, + proofs: z.array(zodHexString), + merkleRoot: zodHexString, + salts: z.string(), + privacy: z.object({ obfuscated: z.array(zodHexString) }), +}); -export const SignedWrappedProof = WrappedProof.And( - Record({ - key: String, - signature: String, +const WrappedSignedProof = WrappedProof.and( + z.object({ + key: z.string(), + signature: z.string(), }) ); -export type SignedWrappedProof = Static; -export type WrappedDocument = T & { - proof: WrappedProof; +type WrappedOpenAttestationVC = T & { + proof: z.infer; }; -export type SignedWrappedDocument = T & { - proof: SignedWrappedProof; +type WrappedSignedOpenAttestationVC = T & { + proof: z.infer; }; -export * from "../__generated__/schema.4.0"; +export { VC, OpenAttestationVC, WrappedOpenAttestationVC, WrappedSignedOpenAttestationVC, Salt }; diff --git a/src/4.0/validate/dataModel.ts b/src/4.0/validate/dataModel.ts index 10d38212..94a7b743 100644 --- a/src/4.0/validate/dataModel.ts +++ b/src/4.0/validate/dataModel.ts @@ -7,9 +7,9 @@ const baseType = "VerifiableCredential"; // Custom URI validation function const URI_REGEX = /^(?=.)(?!https?:\/(?:$|[^/]))(?!https?:\/\/\/)(?!https?:[^/])(?:[a-zA-Z][a-zA-Z\d+-\.]*:(?:(?:\/\/(?:[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:]*@)?(?:\[(?:(?:(?:[\dA-Fa-f]{1,4}:){6}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|::(?:[\dA-Fa-f]{1,4}:){5}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){4}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,1}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){3}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,2}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){2}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,3}[\dA-Fa-f]{1,4})?::[\dA-Fa-f]{1,4}:(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,4}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,5}[\dA-Fa-f]{1,4})?::[\dA-Fa-f]{1,4}|(?:(?:[\dA-Fa-f]{1,4}:){0,6}[\dA-Fa-f]{1,4})?::)|v[\dA-Fa-f]+\.[\w-\.~!\$&'\(\)\*\+,;=:]+)\]|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])|[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=]{1,255})(?::\d*)?(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)|\/(?:[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]+(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)?|[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]+(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*|(?:\/\/\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)))(?:\?[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@\/\?]*(?=#|$))?(?:#[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@\/\?]*)?$/; -const zodUri = z.string().regex(URI_REGEX, { message: "Invalid URI" }); +export const zodUri = z.string().regex(URI_REGEX, { message: "Invalid URI" }); -export const inputVcModel = z.object({ +export const vcDataModel = z.object({ "@context": z.union([ z.record(z.any()), z.string(), diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index 09f2a9ee..aa9edfa6 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -1,27 +1,34 @@ import { hashToBuffer, isStringArray } from "../shared/utils"; import { MerkleTree } from "../shared/merkle"; -import { ContextUrl } from "../shared/@types/document"; -import { WrappedDocument } from "./types"; +import { ContextType, ContextUrl } from "../shared/@types/document"; +import { OpenAttestationVC, WrappedOpenAttestationVC } from "./types"; import { digestCredential } from "../4.0/digest"; -import { WrapDocumentOptionV4 } from "../shared/@types/wrap"; -import { OpenAttestationDocument, ProofPurpose } from "../__generated__/schema.4.0"; import { encodeSalt, salt } from "./salt"; -import { interpretContexts, inputVcModel } from "./validate"; +import { interpretContexts, vcDataModel } from "./validate"; -export const wrapDocument = async ( - credential: T, - options: WrapDocumentOptionV4 // eslint-disable-line @typescript-eslint/no-unused-vars -): Promise> => { - const document = { ...credential }; - - /* 1. Data model validation */ - const result = await inputVcModel.safeParseAsync(document); +export const wrapDocument = async ( + credential: T +): Promise> => { + /* 1a. W3C VC data model validation */ + const result = await vcDataModel.safeParseAsync(credential); if (!result.success) throw new Error( `Input document does not conform to Verifiable Credentials v2.0 Data Model: ${JSON.stringify( result.error.issues )}` ); + const document = result.data; + + /* 1b. Narrow down to OpenAttestation VC validation */ + const oav4context = await OpenAttestationVC.shape["@context"].safeParseAsync(document["@context"]); // Superficial check on user intention + if (oav4context.success) { + const oav4 = await OpenAttestationVC.safeParseAsync(document); + if (!oav4.success) { + throw new Error( + `Input document does not conform to OpenAttestation v4.0 Data Model: ${JSON.stringify(oav4.error.issues)}` + ); + } + } /* 2. Ensure provided @context are interpretable (e.g. valid @context URL, all types are mapped, etc.) */ await interpretContexts(document); @@ -35,18 +42,20 @@ export const wrapDocument = async ( } else if (isStringArray(document["@context"])) { document["@context"].forEach((context) => contexts.add(context)); } - document["@context"] = Array.from(contexts); // Since JavaScript Sets preserve insertion order and duplicated inserts do not affect the order, we can do this + [ContextUrl.v2_vc, ContextUrl.v4_alpha].forEach((c) => contexts.delete(c)); + const finalContexts: OpenAttestationVC["@context"] = [ContextUrl.v2_vc, ContextUrl.v4_alpha, ...Array.from(contexts)]; // Since JavaScript Sets preserve insertion order and duplicated inserts do not affect the order, we can do this /* 4. Type validation */ // Ensure that required types are present and in the correct order // type: ["VerifiableCredential", "OpenAttestationCredential", ...] - const types = new Set(["VerifiableCredential", "OpenAttestationCredential"]); + const types = new Set([ContextType.BaseContext, ContextType.V4AlphaContext]); if (typeof document["type"] === "string") { types.add(document["type"]); } else if (isStringArray(document["type"])) { document["type"].forEach((type) => types.add(type)); } - document["type"] = Array.from(types); // Since JavaScript Sets preserve insertion order and duplicated inserts do not affect the order, we can do this + [ContextUrl.v2_vc, ContextUrl.v4_alpha].forEach((c) => contexts.delete(c)); + // document["type"] = Array.from(types); // Since JavaScript Sets preserve insertion order and duplicated inserts do not affect the order, we can do this /* 5. OA wrapping */ const salts = salt(document); @@ -58,11 +67,17 @@ export const wrapDocument = async ( const merkleRoot = merkleTree.getRoot().toString("hex"); const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer: Buffer) => buffer.toString("hex")); - const verifiableCredential: WrappedDocument = { + const verifiableCredential: WrappedOpenAttestationVC = { ...document, + "@context": finalContexts, + type: [ContextType.BaseContext, ContextType.V4AlphaContext], // FIXME: Follow finalContexts + issuer: document["issuer"] as OpenAttestationVC["issuer"], // Assume valid by asserting types + ...(document["credentialStatus"] + ? { credentialStatus: document["credentialStatus"] as OpenAttestationVC["credentialStatus"] } + : {}), proof: { type: "OpenAttestationMerkleProofSignature2018", - proofPurpose: ProofPurpose.AssertionMethod, + proofPurpose: "assertionMethod", targetHash: digest, proofs: merkleProof, merkleRoot, @@ -76,10 +91,9 @@ export const wrapDocument = async ( return verifiableCredential; }; -export const wrapDocuments = async ( - documents: T[], - options: WrapDocumentOptionV4 -): Promise[]> => { +export const wrapDocuments = async ( + documents: T[] +): Promise[]> => { // create individual verifiable credential const verifiableCredentials = await Promise.all(documents.map((document) => wrapDocument(document, options))); diff --git a/src/index.ts b/src/index.ts index 0f80d9d0..d68ef8fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import * as utils from "./shared/utils"; import { SchemaValidationError } from "./shared/utils"; import { validateSchema as validate } from "./shared/validate"; import { SchemaId, WrappedDocument, OpenAttestationDocument } from "./shared/@types/document"; -import { WrapDocumentOptionV2, WrapDocumentOptionV3, WrapDocumentOptionV4 } from "./shared/@types/wrap"; +import { WrapDocumentOptionV2, WrapDocumentOptionV3 } from "./shared/@types/wrap"; import { SigningKey, SUPPORTED_SIGNING_ALGORITHM } from "./shared/@types/sign"; import * as v2 from "./2.0/types"; @@ -26,13 +26,11 @@ import { obfuscateVerifiableCredential as obfuscateVerifiableCredentialV3 } from import { OpenAttestationDocument as OpenAttestationDocumentV3 } from "./__generated__/schema.3.0"; import * as v4 from "./4.0/types"; -import { WrappedDocument as WrappedDocumentV4 } from "./4.0/types"; import { wrapDocument as wrapDocumentV4, wrapDocuments as wrapDocumentsV4 } from "./4.0/wrap"; import { signDocument as signDocumentV4 } from "./4.0/sign"; import { verify as verifyV4 } from "./4.0/verify"; import { digestCredential as digestCredentialV4 } from "./4.0/digest"; import { obfuscateVerifiableCredential as obfuscateVerifiableCredentialV4 } from "./4.0/obfuscate"; -import { OpenAttestationDocument as OpenAttestationDocumentV4 } from "./__generated__/schema.4.0"; export function wrapDocument( data: T, @@ -62,18 +60,16 @@ export function __unsafe__use__it__at__your__own__risks__wrapDocuments( - data: T, - options?: WrapDocumentOptionV4 -): Promise> { - return wrapDocumentV4(data, options ?? { version: SchemaId.v4 }); +export function _unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocument( + data: T +): Promise> { + return wrapDocumentV4(data); } -export function _unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocuments( - dataArray: T[], - options?: WrapDocumentOptionV4 -): Promise[]> { - return wrapDocumentsV4(dataArray, options ?? { version: SchemaId.v4 }); +export function _unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocuments( + dataArray: T[] +): Promise[]> { + return wrapDocumentsV4(dataArray); } export const validateSchema = (document: WrappedDocument): boolean => { @@ -97,7 +93,7 @@ export function verifySignature( algorithm: SUPPORTED_SIGNING_ALGORITHM, keyOrSigner: SigningKey | ethers.Signer ): Promise>; -export async function signDocument( +export async function signDocument( document: v4.SignedWrappedDocument | v4.WrappedDocument, algorithm: SUPPORTED_SIGNING_ALGORITHM, keyOrSigner: SigningKey | ethers.Signer diff --git a/src/shared/@types/document.ts b/src/shared/@types/document.ts index 0a62aa56..0d8c8bec 100644 --- a/src/shared/@types/document.ts +++ b/src/shared/@types/document.ts @@ -36,13 +36,17 @@ export type SignedWrappedDocument = T extends export enum SchemaId { v2 = "https://schema.openattestation.com/2.0/schema.json", v3 = "https://schema.openattestation.com/3.0/schema.json", - v4 = "https://schemata.openattestation.com/com/openattestation/4.0/alpha-schema.json", // Note: Schema property is no longer placed in the OA v4 document } -export enum ContextUrl { - v2_vc = "https://www.w3.org/ns/credentials/v2", - v4_alpha = "https://schemata.openattestation.com/com/openattestation/4.0/alpha-context.json", -} +export const ContextUrl = { + v2_vc: "https://www.w3.org/ns/credentials/v2", + v4_alpha: "https://schemata.openattestation.com/com/openattestation/4.0/alpha-context.json", +} as const; + +export const ContextType = { + BaseContext: "VerifiableCredential", + V4AlphaContext: "OpenAttestationCredential", +} as const; export const OpenAttestationHexString = String.withConstraint( (value) => ethers.utils.isHexString(`0x${value}`, 32) || `${value} has not the expected length of 32 bytes` diff --git a/src/shared/@types/wrap.ts b/src/shared/@types/wrap.ts index 6d0c3244..8fa54e04 100644 --- a/src/shared/@types/wrap.ts +++ b/src/shared/@types/wrap.ts @@ -12,7 +12,3 @@ export interface WrapDocumentOptionV3 { externalSchemaId?: string; version: SchemaId.v3; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface WrapDocumentOptionV4 { - // If any, add options to wrap utility -} diff --git a/src/shared/utils/guard.ts b/src/shared/utils/guard.ts index ffd0dedd..0f8f03d1 100644 --- a/src/shared/utils/guard.ts +++ b/src/shared/utils/guard.ts @@ -7,10 +7,7 @@ import { OpenAttestationDocument as OpenAttestationDocumentV3, WrappedDocument as WrappedDocumentV3, } from "../../3.0/types"; -import { - OpenAttestationDocument as OpenAttestationDocumentV4, - WrappedDocument as WrappedDocumentV4, -} from "../../4.0/types"; +import { OpenAttestationVC as OpenAttestationDocumentV4, WrappedDocument as WrappedDocumentV4 } from "../../4.0/types"; import { diagnose } from "./diagnose"; import { Mode } from "./@types/diagnose"; diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index 50670550..b6264d70 100644 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -14,7 +14,7 @@ import * as v4 from "../../__generated__/schema.4.0"; import { WrappedDocument as WrappedDocumentV4 } from "../../4.0/types"; import { OpenAttestationDocument as OpenAttestationDocumentV4 } from "../../__generated__/schema.4.0"; -import { OpenAttestationDocument, WrappedDocument } from "../@types/document"; +import { OpenAttestationDocument, WrappedDocument, SchemaId, ContextUrl, ContextType } from "../@types/document"; import { isRawV2Document, isWrappedV2Document, @@ -261,3 +261,22 @@ export const getObfuscatedData = ( export const isStringArray = (input: unknown): input is string[] => Array.isArray(input) && input.every((i) => typeof i === "string"); + +export const getVersion = (document: unknown) => { + if (typeof document === "object" && document !== null) { + if ("version" in document && typeof document.version === "string") { + switch (document.version) { + case SchemaId.v2: + return 2; + case SchemaId.v3: + return 3; + } + } else if ("@context" in document && Array.isArray(document["@context"])) { + if (document["@context"].includes(ContextUrl.v4_alpha)) { + return 4; + } + } + } + + throw new Error("Unknown document version: Can only determine between OpenAttestation v2, v3 & v4 documents."); +}; From b102ee9d53828b5609000b4d0460d01a49558710 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 16:30:51 +0800 Subject: [PATCH 02/43] refactor: prefer string union --- src/4.0/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/4.0/types.ts b/src/4.0/types.ts index d4e67410..d333641e 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -2,9 +2,9 @@ import z from "zod"; // import { OpenAttestationDocument as OpenAttestationDocumentV4, ProofPurpose } from "../__generated__/schema.4.0"; import { vcDataModel, zodUri } from "./validate/dataModel"; -import { ContextUrl, ContextType, OpenAttestationHexString, SignatureAlgorithm } from "../shared/@types/document"; +import { ContextUrl, ContextType } from "../shared/@types/document"; -const IdentityProofType = z.enum(["DNS-TXT", "DNS-DID", "DID"]); +const IdentityProofType = z.union([z.literal("DNS-TXT"), z.literal("DNS-DID"), z.literal("DID")]); type IdentityProofType = z.infer; const Salt = z.object({ value: z.string(), path: z.string() }); From e693b92fc55040ff3194dc16f0f34af0c1bc17a5 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 16:40:40 +0800 Subject: [PATCH 03/43] refactor: reference constant tuple instead --- src/4.0/wrap.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index aa9edfa6..bbe7f661 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -36,14 +36,15 @@ export const wrapDocument = async ( /* 3. Context validation */ // Ensure that required contexts are present and in the correct order // type: [Base, OA, ...] - const contexts = new Set([ContextUrl["v2_vc"], ContextUrl["v4_alpha"]]); + const REQUIRED_CONTEXTS = [ContextUrl.v2_vc, ContextUrl.v4_alpha] as const; + const contexts = new Set(REQUIRED_CONTEXTS); if (typeof document["@context"] === "string") { contexts.add(document["@context"]); } else if (isStringArray(document["@context"])) { document["@context"].forEach((context) => contexts.add(context)); } - [ContextUrl.v2_vc, ContextUrl.v4_alpha].forEach((c) => contexts.delete(c)); - const finalContexts: OpenAttestationVC["@context"] = [ContextUrl.v2_vc, ContextUrl.v4_alpha, ...Array.from(contexts)]; // Since JavaScript Sets preserve insertion order and duplicated inserts do not affect the order, we can do this + REQUIRED_CONTEXTS.forEach((c) => contexts.delete(c)); + const finalContexts: OpenAttestationVC["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; // Since JavaScript Sets preserve insertion order and duplicated inserts do not affect the order, we can do this /* 4. Type validation */ // Ensure that required types are present and in the correct order From c4c91febb1aa86661a9af51fcd6535ac08cad683 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 16:53:52 +0800 Subject: [PATCH 04/43] refactor: finalTypes follows pattern of finalContexts --- src/4.0/wrap.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index bbe7f661..990172e1 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -44,19 +44,20 @@ export const wrapDocument = async ( document["@context"].forEach((context) => contexts.add(context)); } REQUIRED_CONTEXTS.forEach((c) => contexts.delete(c)); - const finalContexts: OpenAttestationVC["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; // Since JavaScript Sets preserve insertion order and duplicated inserts do not affect the order, we can do this + const finalContexts: OpenAttestationVC["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; /* 4. Type validation */ // Ensure that required types are present and in the correct order // type: ["VerifiableCredential", "OpenAttestationCredential", ...] + const REQUIRED_TYPES = [ContextType.BaseContext, ContextType.V4AlphaContext] as const; const types = new Set([ContextType.BaseContext, ContextType.V4AlphaContext]); if (typeof document["type"] === "string") { types.add(document["type"]); } else if (isStringArray(document["type"])) { - document["type"].forEach((type) => types.add(type)); + types.forEach((type) => types.add(type)); } - [ContextUrl.v2_vc, ContextUrl.v4_alpha].forEach((c) => contexts.delete(c)); - // document["type"] = Array.from(types); // Since JavaScript Sets preserve insertion order and duplicated inserts do not affect the order, we can do this + REQUIRED_TYPES.forEach((t) => types.delete(t)); + const finalTypes: OpenAttestationVC["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; /* 5. OA wrapping */ const salts = salt(document); From 5ff08a335fa5d5b1fbe1a36ef94dfe39c7de826c Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 16:54:57 +0800 Subject: [PATCH 05/43] refactor: document -> raw document for clarity --- src/4.0/wrap.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index 990172e1..04f251ce 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -17,12 +17,12 @@ export const wrapDocument = async ( result.error.issues )}` ); - const document = result.data; + const rawDocument = result.data; /* 1b. Narrow down to OpenAttestation VC validation */ - const oav4context = await OpenAttestationVC.shape["@context"].safeParseAsync(document["@context"]); // Superficial check on user intention + const oav4context = await OpenAttestationVC.shape["@context"].safeParseAsync(rawDocument["@context"]); // Superficial check on user intention if (oav4context.success) { - const oav4 = await OpenAttestationVC.safeParseAsync(document); + const oav4 = await OpenAttestationVC.safeParseAsync(rawDocument); if (!oav4.success) { throw new Error( `Input document does not conform to OpenAttestation v4.0 Data Model: ${JSON.stringify(oav4.error.issues)}` @@ -31,17 +31,17 @@ export const wrapDocument = async ( } /* 2. Ensure provided @context are interpretable (e.g. valid @context URL, all types are mapped, etc.) */ - await interpretContexts(document); + await interpretContexts(rawDocument); /* 3. Context validation */ // Ensure that required contexts are present and in the correct order // type: [Base, OA, ...] const REQUIRED_CONTEXTS = [ContextUrl.v2_vc, ContextUrl.v4_alpha] as const; const contexts = new Set(REQUIRED_CONTEXTS); - if (typeof document["@context"] === "string") { - contexts.add(document["@context"]); - } else if (isStringArray(document["@context"])) { - document["@context"].forEach((context) => contexts.add(context)); + if (typeof rawDocument["@context"] === "string") { + contexts.add(rawDocument["@context"]); + } else if (isStringArray(rawDocument["@context"])) { + rawDocument["@context"].forEach((context) => contexts.add(context)); } REQUIRED_CONTEXTS.forEach((c) => contexts.delete(c)); const finalContexts: OpenAttestationVC["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; @@ -51,17 +51,17 @@ export const wrapDocument = async ( // type: ["VerifiableCredential", "OpenAttestationCredential", ...] const REQUIRED_TYPES = [ContextType.BaseContext, ContextType.V4AlphaContext] as const; const types = new Set([ContextType.BaseContext, ContextType.V4AlphaContext]); - if (typeof document["type"] === "string") { - types.add(document["type"]); - } else if (isStringArray(document["type"])) { + if (typeof rawDocument["type"] === "string") { + types.add(rawDocument["type"]); + } else if (isStringArray(rawDocument["type"])) { types.forEach((type) => types.add(type)); } REQUIRED_TYPES.forEach((t) => types.delete(t)); const finalTypes: OpenAttestationVC["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; /* 5. OA wrapping */ - const salts = salt(document); - const digest = digestCredential(document, salts, []); + const salts = salt(rawDocument); + const digest = digestCredential(rawDocument, salts, []); const batchBuffers = [digest].map(hashToBuffer); @@ -70,12 +70,12 @@ export const wrapDocument = async ( const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer: Buffer) => buffer.toString("hex")); const verifiableCredential: WrappedOpenAttestationVC = { - ...document, + ...rawDocument, "@context": finalContexts, type: [ContextType.BaseContext, ContextType.V4AlphaContext], // FIXME: Follow finalContexts - issuer: document["issuer"] as OpenAttestationVC["issuer"], // Assume valid by asserting types - ...(document["credentialStatus"] - ? { credentialStatus: document["credentialStatus"] as OpenAttestationVC["credentialStatus"] } + issuer: rawDocument["issuer"] as OpenAttestationVC["issuer"], // Assume valid by asserting types + ...(rawDocument["credentialStatus"] + ? { credentialStatus: rawDocument["credentialStatus"] as OpenAttestationVC["credentialStatus"] } : {}), proof: { type: "OpenAttestationMerkleProofSignature2018", From 23af4384156daaa9fda052e144bc6766b45b149e Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 17:04:06 +0800 Subject: [PATCH 06/43] fix: rawDocument should not be used directly for digest and salting --- src/4.0/wrap.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index 04f251ce..77b8d7e4 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -1,7 +1,7 @@ import { hashToBuffer, isStringArray } from "../shared/utils"; import { MerkleTree } from "../shared/merkle"; import { ContextType, ContextUrl } from "../shared/@types/document"; -import { OpenAttestationVC, WrappedOpenAttestationVC } from "./types"; +import { OpenAttestationVC, VC, WrappedOpenAttestationVC } from "./types"; import { digestCredential } from "../4.0/digest"; import { encodeSalt, salt } from "./salt"; import { interpretContexts, vcDataModel } from "./validate"; @@ -59,9 +59,15 @@ export const wrapDocument = async ( REQUIRED_TYPES.forEach((t) => types.delete(t)); const finalTypes: OpenAttestationVC["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; + const documentReadyForWrapping = { + ...rawDocument, + "@context": finalContexts, + type: finalTypes, + } satisfies VC; + /* 5. OA wrapping */ - const salts = salt(rawDocument); - const digest = digestCredential(rawDocument, salts, []); + const salts = salt(documentReadyForWrapping); + const digest = digestCredential(documentReadyForWrapping, salts, []); const batchBuffers = [digest].map(hashToBuffer); @@ -70,8 +76,7 @@ export const wrapDocument = async ( const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer: Buffer) => buffer.toString("hex")); const verifiableCredential: WrappedOpenAttestationVC = { - ...rawDocument, - "@context": finalContexts, + ...documentReadyForWrapping, type: [ContextType.BaseContext, ContextType.V4AlphaContext], // FIXME: Follow finalContexts issuer: rawDocument["issuer"] as OpenAttestationVC["issuer"], // Assume valid by asserting types ...(rawDocument["credentialStatus"] From 5cc50cfe6acc2e0785c1a4317a39d1644e86f652 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 17:16:06 +0800 Subject: [PATCH 07/43] fix: remove asserting to string to let the literal infer properly --- src/4.0/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/4.0/types.ts b/src/4.0/types.ts index d333641e..c264090a 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -23,7 +23,7 @@ const OpenAttestationVC = vcDataModel.extend({ type: z // Must be an array that starts with [VerifiableCredential, OpenAttestationCredential, ...] - .tuple([z.literal(ContextType.BaseContext as string), z.literal(ContextType.V4AlphaContext as string)]) + .tuple([z.literal(ContextType.BaseContext), z.literal(ContextType.V4AlphaContext)]) // Remaining items can be string .rest(z.string()), From e7e4c72a1403fea612712537bf557598bcff17aa Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 18:26:51 +0800 Subject: [PATCH 08/43] fix: typings --- src/4.0/wrap.ts | 41 ++++++++--------------------------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index 77b8d7e4..1e34d7d6 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -74,14 +74,9 @@ export const wrapDocument = async ( const merkleTree = new MerkleTree(batchBuffers); const merkleRoot = merkleTree.getRoot().toString("hex"); const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer: Buffer) => buffer.toString("hex")); - const verifiableCredential: WrappedOpenAttestationVC = { ...documentReadyForWrapping, - type: [ContextType.BaseContext, ContextType.V4AlphaContext], // FIXME: Follow finalContexts - issuer: rawDocument["issuer"] as OpenAttestationVC["issuer"], // Assume valid by asserting types - ...(rawDocument["credentialStatus"] - ? { credentialStatus: rawDocument["credentialStatus"] as OpenAttestationVC["credentialStatus"] } - : {}), + ...assertAsOaVcProps(documentReadyForWrapping, ["issuer", "credentialStatus"]), proof: { type: "OpenAttestationMerkleProofSignature2018", proofPurpose: "assertionMethod", @@ -95,33 +90,13 @@ export const wrapDocument = async ( }, }; - return verifiableCredential; + return verifiableCredential as WrappedOpenAttestationVC; }; -export const wrapDocuments = async ( - documents: T[] -): Promise[]> => { - // create individual verifiable credential - const verifiableCredentials = await Promise.all(documents.map((document) => wrapDocument(document, options))); - - // get all the target hashes to compute the merkle tree and the merkle root - const merkleTree = new MerkleTree( - verifiableCredentials.map((verifiableCredential) => verifiableCredential.proof.targetHash).map(hashToBuffer) - ); - const merkleRoot = merkleTree.getRoot().toString("hex"); - - // for each document, update the merkle root and add the proofs needed - return verifiableCredentials.map((verifiableCredential) => { - const digest = verifiableCredential.proof.targetHash; - const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer: Buffer) => buffer.toString("hex")); - - return { - ...verifiableCredential, - proof: { - ...verifiableCredential.proof, - proofs: merkleProof, - merkleRoot, - }, - }; +function assertAsOaVcProps(obj: VC, keys: K[]) { + const temp: Record = {}; + Object.entries(obj).forEach(([k, v]) => { + if (keys.includes(k as K)) temp[k] = v; }); -}; + return temp as { [key in K]: OpenAttestationVC[key] }; +} From 27e6e8e9eafbf4055b70e963f5102a51d58653d5 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 18:49:05 +0800 Subject: [PATCH 09/43] refactor: move assertion earlier --- src/4.0/wrap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index 1e34d7d6..8152c962 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -61,6 +61,7 @@ export const wrapDocument = async ( const documentReadyForWrapping = { ...rawDocument, + ...assertAsOaVcProps(rawDocument, ["issuer", "credentialStatus"]), "@context": finalContexts, type: finalTypes, } satisfies VC; @@ -76,7 +77,6 @@ export const wrapDocument = async ( const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer: Buffer) => buffer.toString("hex")); const verifiableCredential: WrappedOpenAttestationVC = { ...documentReadyForWrapping, - ...assertAsOaVcProps(documentReadyForWrapping, ["issuer", "credentialStatus"]), proof: { type: "OpenAttestationMerkleProofSignature2018", proofPurpose: "assertionMethod", From 2bc4066b645abc1a292fcf1e996ad257ce293c2d Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 19:18:59 +0800 Subject: [PATCH 10/43] refactor: assert oa vc than try vc --- src/4.0/wrap.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index 8152c962..173fae07 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -9,18 +9,9 @@ import { interpretContexts, vcDataModel } from "./validate"; export const wrapDocument = async ( credential: T ): Promise> => { - /* 1a. W3C VC data model validation */ - const result = await vcDataModel.safeParseAsync(credential); - if (!result.success) - throw new Error( - `Input document does not conform to Verifiable Credentials v2.0 Data Model: ${JSON.stringify( - result.error.issues - )}` - ); - const rawDocument = result.data; - - /* 1b. Narrow down to OpenAttestation VC validation */ - const oav4context = await OpenAttestationVC.shape["@context"].safeParseAsync(rawDocument["@context"]); // Superficial check on user intention + /* 1a. try OpenAttestation VC validation, since most user will be issuing oa v4*/ + const oav4context = await OpenAttestationVC.pick({ "@context": true }).safeParseAsync(credential); // Superficial check on user intention + let rawDocument: VC | undefined; if (oav4context.success) { const oav4 = await OpenAttestationVC.safeParseAsync(rawDocument); if (!oav4.success) { @@ -28,6 +19,20 @@ export const wrapDocument = async ( `Input document does not conform to OpenAttestation v4.0 Data Model: ${JSON.stringify(oav4.error.issues)}` ); } + rawDocument = oav4.data; + } + + /* 1b. only if OA VC validation fail do we continue with W3C VC data model validation */ + if (!rawDocument) { + const result = await vcDataModel.safeParseAsync(credential); + if (!result.success) + throw new Error( + `Input document does not conform to Verifiable Credentials v2.0 Data Model: ${JSON.stringify( + result.error.issues + )}` + ); + + rawDocument = result.data; } /* 2. Ensure provided @context are interpretable (e.g. valid @context URL, all types are mapped, etc.) */ From 5fa75613869a6164c4969338be4234641dd553fa Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 19:26:37 +0800 Subject: [PATCH 11/43] fix: refer to new oa vc type in digest and verify --- src/4.0/digest.ts | 5 ++--- src/4.0/verify.ts | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/4.0/digest.ts b/src/4.0/digest.ts index 4cfc7359..e171804f 100644 --- a/src/4.0/digest.ts +++ b/src/4.0/digest.ts @@ -1,9 +1,8 @@ import { get, sortBy } from "lodash"; import { keccak256 } from "js-sha3"; -import { Salt } from "./types"; -import { OpenAttestationDocument } from "../__generated__/schema.4.0"; +import { OpenAttestationVC, Salt } from "./types"; -export const digestCredential = (document: OpenAttestationDocument, salts: Salt[], obfuscatedData: string[]) => { +export const digestCredential = (document: OpenAttestationVC, salts: Salt[], obfuscatedData: string[]) => { // Prepare array of hashes from visible data const hashedUnhashedDataArray = salts // Explictly allow falsy values (e.g. false, 0, etc.) as they can exist in the document diff --git a/src/4.0/verify.ts b/src/4.0/verify.ts index ac413a25..3bb7d97c 100644 --- a/src/4.0/verify.ts +++ b/src/4.0/verify.ts @@ -1,9 +1,11 @@ -import { WrappedDocument } from "./types"; +import { WrappedSignedOpenAttestationVC } from "./types"; import { digestCredential } from "./digest"; import { checkProof } from "../shared/merkle"; import { decodeSalt, salt } from "./salt"; -export const verify = (document: T): document is WrappedDocument => { +export const verify = ( + document: T +): document is WrappedSignedOpenAttestationVC => { if (!document.proof) { return false; } From 9ce12558a48800673dff709ed788d443fbe59385 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 19:31:20 +0800 Subject: [PATCH 12/43] fix: refer to new oa vc typings --- src/4.0/obfuscate.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/4.0/obfuscate.ts b/src/4.0/obfuscate.ts index 423c352a..90caa0f3 100644 --- a/src/4.0/obfuscate.ts +++ b/src/4.0/obfuscate.ts @@ -1,11 +1,10 @@ -import { OpenAttestationDocument } from "../__generated__/schema.4.0"; import { toBuffer } from "../shared/utils"; -import { WrappedDocument } from "./types"; import { cloneDeep, get, unset, pick } from "lodash"; import { decodeSalt, encodeSalt } from "./salt"; import { traverseAndFlatten } from "./traverseAndFlatten"; +import { OpenAttestationVC, WrappedOpenAttestationVC } from "./types"; -const obfuscate = (_data: WrappedDocument, fields: string[] | string) => { +const obfuscate = (_data: WrappedOpenAttestationVC, fields: string[] | string) => { const data = cloneDeep(_data); // Prevents alteration of original data const fieldsAsArray = ([] as string[]).concat(fields); @@ -37,10 +36,10 @@ const obfuscate = (_data: WrappedDocument, fields: stri }; }; -export const obfuscateVerifiableCredential = ( - document: WrappedDocument, +export const obfuscateVerifiableCredential = ( + document: WrappedOpenAttestationVC, fields: string[] | string -): WrappedDocument => { +): WrappedOpenAttestationVC => { const { data, obfuscatedData } = obfuscate(document, fields); const currentObfuscatedData = document.proof.privacy.obfuscated; const newObfuscatedData = currentObfuscatedData.concat(obfuscatedData); @@ -53,5 +52,5 @@ export const obfuscateVerifiableCredential = ( obfuscated: newObfuscatedData, }, }, - }; + } satisfies WrappedOpenAttestationVC as WrappedOpenAttestationVC; }; From e6bff0776d0a97b4d1dd1c5f02a7faadc2d7132b Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 19:49:44 +0800 Subject: [PATCH 13/43] fix: refer to new oa vc typings --- src/4.0/sign.ts | 10 +++++----- src/shared/utils/guard.ts | 18 +++++++++++------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/4.0/sign.ts b/src/4.0/sign.ts index 7ba89797..31f7c0f5 100644 --- a/src/4.0/sign.ts +++ b/src/4.0/sign.ts @@ -1,18 +1,18 @@ -import { OpenAttestationVC, WrappedDocument, SignedWrappedDocument, SignedWrappedProof } from "./types"; import { sign } from "../shared/signer"; import { SigningKey, SUPPORTED_SIGNING_ALGORITHM } from "../shared/@types/sign"; import { isSignedWrappedV4Document } from "../shared/utils"; import { ethers } from "ethers"; +import { OpenAttestationVC, WrappedOpenAttestationVC, WrappedSignedOpenAttestationVC } from "./types"; -export const signDocument = async ( - document: SignedWrappedDocument | WrappedDocument, +export const signDocument = async ( + document: WrappedSignedOpenAttestationVC | WrappedOpenAttestationVC, algorithm: SUPPORTED_SIGNING_ALGORITHM, keyOrSigner: SigningKey | ethers.Signer -): Promise> => { +): Promise> => { if (isSignedWrappedV4Document(document)) throw new Error("Document has been signed"); const merkleRoot = `0x${document.proof.merkleRoot}`; const signature = await sign(algorithm, merkleRoot, keyOrSigner); - const proof: SignedWrappedProof = { + const proof: WrappedSignedOpenAttestationVC["proof"] = { ...document.proof, key: SigningKey.guard(keyOrSigner) ? keyOrSigner.public : `did:ethr:${await keyOrSigner.getAddress()}#controller`, signature, diff --git a/src/shared/utils/guard.ts b/src/shared/utils/guard.ts index 0f8f03d1..be2dea4d 100644 --- a/src/shared/utils/guard.ts +++ b/src/shared/utils/guard.ts @@ -7,7 +7,11 @@ import { OpenAttestationDocument as OpenAttestationDocumentV3, WrappedDocument as WrappedDocumentV3, } from "../../3.0/types"; -import { OpenAttestationVC as OpenAttestationDocumentV4, WrappedDocument as WrappedDocumentV4 } from "../../4.0/types"; +import { + OpenAttestationVC as OpenAttestationDocumentV4, + WrappedOpenAttestationVC, + WrappedSignedOpenAttestationVC, +} from "../../4.0/types"; import { diagnose } from "./diagnose"; import { Mode } from "./@types/diagnose"; @@ -76,10 +80,10 @@ export const isWrappedV3Document = ( * @param document * @param mode strict or non-strict. Strict will perform additional check on the data. For instance strict validation will ensure that a target hash is a 32 bytes hex string while non strict validation will just check that target hash is a string. */ -export const isWrappedV4Document = ( - document: any, +export const isWrappedV4Document = ( + document: unknown, { mode }: { mode: Mode } = { mode: "non-strict" } -): document is WrappedDocumentV4 => { +): document is T => { return diagnose({ version: "4.0", kind: "wrapped", document, debug: false, mode }).length === 0; }; @@ -112,9 +116,9 @@ export const isSignedWrappedV3Document = ( * @param document * @param mode strict or non-strict. Strict will perform additional check on the data. For instance strict validation will ensure that a target hash is a 32 bytes hex string while non strict validation will just check that target hash is a string. */ -export const isSignedWrappedV4Document = ( - document: any, +export const isSignedWrappedV4Document = ( + document: unknown, { mode }: { mode: Mode } = { mode: "non-strict" } -): document is SignedWrappedDocument => { +): document is T => { return diagnose({ version: "4.0", kind: "signed", document, debug: false, mode }).length === 0; }; From a259eaca3b97849860b50a2afa00b87c14d7acbb Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 20:35:54 +0800 Subject: [PATCH 14/43] fix: refer to new oa vc typings --- src/shared/utils/utils.ts | 44 +++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index b6264d70..5fb6248a 100644 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -10,11 +10,9 @@ import * as v3 from "../../__generated__/schema.3.0"; import { WrappedDocument as WrappedDocumentV3 } from "../../3.0/types"; import { OpenAttestationDocument as OpenAttestationDocumentV3 } from "../../__generated__/schema.3.0"; -import * as v4 from "../../__generated__/schema.4.0"; -import { WrappedDocument as WrappedDocumentV4 } from "../../4.0/types"; -import { OpenAttestationDocument as OpenAttestationDocumentV4 } from "../../__generated__/schema.4.0"; +import { WrappedOpenAttestationVC as WrappedDocumentV4 } from "../../4.0/types"; -import { OpenAttestationDocument, WrappedDocument, SchemaId, ContextUrl, ContextType } from "../@types/document"; +import { OpenAttestationDocument, WrappedDocument, SchemaId, ContextUrl } from "../@types/document"; import { isRawV2Document, isWrappedV2Document, @@ -24,6 +22,13 @@ import { isWrappedV4Document, } from "./guard"; +const VersionGuard = { + 2: isWrappedV2Document, + 3: isWrappedV3Document, + 4: isWrappedV4Document, +} as const satisfies Record boolean>; +type OAVersion = keyof typeof VersionGuard; + export type Hash = string | Buffer; type Extract

= P extends WrappedDocumentV2 ? T : never; export const getData = >(document: T): Extract => { @@ -101,13 +106,20 @@ export function getIssuerAddress(document: any): any { } export const getMerkleRoot = (document: any): string => { - if (isWrappedV2Document(document)) return document.signature.merkleRoot; - else if (isWrappedV3Document(document)) return document.proof.merkleRoot; - else if (isWrappedV4Document(document)) return document.proof.merkleRoot; - - throw new Error( - "Unsupported document type: Only can retrieve merkle root from wrapped OpenAttestation v2, v3 & v4 documents." - ); + const version = getVersion(document); + const getMerkleRoot: Record unknown | undefined> = { + 2: () => document?.signature?.merkleRoot, + 3: () => document?.proof?.merkleRoot, + 4: () => document?.proof?.merkleRoot, + }; + + const merkleRoot = getMerkleRoot[version](); + if (merkleRoot === undefined || typeof merkleRoot !== "string") + throw new Error( + "Unsupported document type: Only can retrieve merkle root from wrapped OpenAttestation v2, v3 & v4 documents." + ); + + return merkleRoot; }; export const getTargetHash = (document: any): string => { @@ -187,8 +199,8 @@ export const isDocumentRevokable = (document: any): boolean => { } else if (isWrappedV4Document(document)) { if (typeof document.issuer === "string" || !document.credentialStatus) return false; const isDidRevokableV4 = - document.issuer.identityProof?.identityProofType === v4.IdentityProofType.DNSDid - ? document.credentialStatus.type === "OpenAttestationOcspResponder" // TODO: Create suggested typings e.g. "v4.CredentialStatusType.OpenAttestationOcspResponder" + document.issuer.identityProof?.identityProofType === "DNS-DID" + ? document.credentialStatus.type === "OpenAttestationOcspResponder" : false; // TODO: OA v4 issuer schema not updated to support document store issuance yet // const isDocumentStoreRevokableV4 = ? @@ -225,7 +237,7 @@ export const isObfuscated = ( document: | WrappedDocumentV2 | WrappedDocumentV3 - | WrappedDocumentV4 + | WrappedDocumentV4 ): boolean => { if (isWrappedV2Document(document)) { return !!document.privacy?.obfuscatedData?.length; @@ -244,7 +256,7 @@ export const getObfuscatedData = ( document: | WrappedDocumentV2 | WrappedDocumentV3 - | WrappedDocumentV4 + | WrappedDocumentV4 ): string[] => { if (isWrappedV2Document(document)) { return document.privacy?.obfuscatedData || []; @@ -262,7 +274,7 @@ export const getObfuscatedData = ( export const isStringArray = (input: unknown): input is string[] => Array.isArray(input) && input.every((i) => typeof i === "string"); -export const getVersion = (document: unknown) => { +export const getVersion = (document: unknown): OAVersion => { if (typeof document === "object" && document !== null) { if ("version" in document && typeof document.version === "string") { switch (document.version) { From 8dfd6af975955e6c5b9b743bfb9138eda6bdf6af Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 20:39:31 +0800 Subject: [PATCH 15/43] refactor: improve naming consistency --- src/4.0/wrap.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index 173fae07..fe4b2db8 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -24,15 +24,13 @@ export const wrapDocument = async ( /* 1b. only if OA VC validation fail do we continue with W3C VC data model validation */ if (!rawDocument) { - const result = await vcDataModel.safeParseAsync(credential); - if (!result.success) + const vc = await vcDataModel.safeParseAsync(credential); + if (!vc.success) throw new Error( - `Input document does not conform to Verifiable Credentials v2.0 Data Model: ${JSON.stringify( - result.error.issues - )}` + `Input document does not conform to Verifiable Credentials v2.0 Data Model: ${JSON.stringify(vc.error.issues)}` ); - rawDocument = result.data; + rawDocument = vc.data; } /* 2. Ensure provided @context are interpretable (e.g. valid @context URL, all types are mapped, etc.) */ From 9fce84aabf755b129ce41d5a6486ae17cee31a1b Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Wed, 24 Apr 2024 21:48:50 +0800 Subject: [PATCH 16/43] fix: typing, should be looser --- src/4.0/verify.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/4.0/verify.ts b/src/4.0/verify.ts index 3bb7d97c..1f1a0046 100644 --- a/src/4.0/verify.ts +++ b/src/4.0/verify.ts @@ -1,11 +1,9 @@ -import { WrappedSignedOpenAttestationVC } from "./types"; +import { WrappedOpenAttestationVC } from "./types"; import { digestCredential } from "./digest"; import { checkProof } from "../shared/merkle"; import { decodeSalt, salt } from "./salt"; -export const verify = ( - document: T -): document is WrappedSignedOpenAttestationVC => { +export const verify = (document: T): document is T => { if (!document.proof) { return false; } From d409c68a966f59469048a9fe3c5be70b3c554cf6 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Thu, 25 Apr 2024 01:06:02 +0800 Subject: [PATCH 17/43] fix: a union b does not override a props with b props --- src/4.0/types.ts | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/4.0/types.ts b/src/4.0/types.ts index c264090a..1b12d57e 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -90,12 +90,37 @@ const WrappedSignedProof = WrappedProof.and( }) ); -type WrappedOpenAttestationVC = T & { - proof: z.infer; -}; +const WrappedDocument = OpenAttestationVC.extend({ + proof: WrappedProof, +}); + +const SignedDocument = OpenAttestationVC.extend({ + proof: WrappedSignedProof, +}); + +type _WrappedDocument = z.infer; +type WrappedOpenAttestationVC< + T extends OpenAttestationVC = OpenAttestationVC, + U extends _WrappedDocument = Override< + T, + { + proof: z.infer; + } + > +> = U; + +type _SignedDocument = z.infer; +type WrappedSignedOpenAttestationVC< + T extends OpenAttestationVC = OpenAttestationVC, + U extends _SignedDocument = Override< + T, + { + proof: z.infer; + } + > +> = U; -type WrappedSignedOpenAttestationVC = T & { - proof: z.infer; -}; +// a & b does not override a props with b props +type Override, U extends Record> = Omit & U; export { VC, OpenAttestationVC, WrappedOpenAttestationVC, WrappedSignedOpenAttestationVC, Salt }; From e30c8e8908be18a646ebc35d8a66bf1f6d0d19d7 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Thu, 25 Apr 2024 11:51:35 +0800 Subject: [PATCH 18/43] fix: added assertion type to prevent accident extension to base type --- src/4.0/types.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/4.0/types.ts b/src/4.0/types.ts index 1b12d57e..49414b04 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -1,6 +1,5 @@ import z from "zod"; -// import { OpenAttestationDocument as OpenAttestationDocumentV4, ProofPurpose } from "../__generated__/schema.4.0"; import { vcDataModel, zodUri } from "./validate/dataModel"; import { ContextUrl, ContextType } from "../shared/@types/document"; @@ -16,6 +15,7 @@ const zodHexString = z.string().regex(HEX_STRING_REGEX, { message: "Invalid Hex const OpenAttestationVC = vcDataModel.extend({ "@context": z + // Must be an array that starts with [baseContext, v4Context, ...] .tuple([z.literal(ContextUrl.v2_vc), z.literal(ContextUrl.v4_alpha)]) // Remaining items can be string or object @@ -71,7 +71,10 @@ const OpenAttestationVC = vcDataModel.extend({ .optional(), }); type VC = z.infer; -type OpenAttestationVC = z.infer; +// AssertStricter is used to ensure that we have zod extended from the base type while +// still being assignable to the base type. For example, if we accidentally extend and +// replaced '@context' to a boolean, this would fail the assertion. +type OpenAttestationVC = AssertStricter>; const WrappedProof = z.object({ type: z.literal("OpenAttestationMerkleProofSignature2018"), @@ -120,7 +123,15 @@ type WrappedSignedOpenAttestationVC< > > = U; -// a & b does not override a props with b props -type Override, U extends Record> = Omit & U; +/** Overrides properties in the Target (a & b does not override a props with b props) */ +type Override, OverrideWith extends Record> = Omit< + Target, + keyof OverrideWith +> & + OverrideWith; + +/** Used to assert that StricterType is a stricter version of LooserType, and most importantly, that + * StricterType is STILL assignable to LooserType. */ +type AssertStricter = StricterType extends LooserType ? StricterType : never; export { VC, OpenAttestationVC, WrappedOpenAttestationVC, WrappedSignedOpenAttestationVC, Salt }; From 194f19efb9a23d0b26256f6edb5de37d094ee5e6 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Thu, 25 Apr 2024 11:55:45 +0800 Subject: [PATCH 19/43] refactor: wording --- src/4.0/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/4.0/types.ts b/src/4.0/types.ts index 49414b04..e1bf3c1c 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -71,10 +71,10 @@ const OpenAttestationVC = vcDataModel.extend({ .optional(), }); type VC = z.infer; -// AssertStricter is used to ensure that we have zod extended from the base type while +// AssertStricterOrEqual is used to ensure that we have zod extended from the base type while // still being assignable to the base type. For example, if we accidentally extend and // replaced '@context' to a boolean, this would fail the assertion. -type OpenAttestationVC = AssertStricter>; +type OpenAttestationVC = AssertStricterOrEqual>; const WrappedProof = z.object({ type: z.literal("OpenAttestationMerkleProofSignature2018"), @@ -130,8 +130,8 @@ type Override, OverrideWith extends Recor > & OverrideWith; -/** Used to assert that StricterType is a stricter version of LooserType, and most importantly, that +/** Used to assert that StricterType is a stricter or equal version of LooserType, and most importantly, that * StricterType is STILL assignable to LooserType. */ -type AssertStricter = StricterType extends LooserType ? StricterType : never; +type AssertStricterOrEqual = StricterType extends LooserType ? StricterType : never; export { VC, OpenAttestationVC, WrappedOpenAttestationVC, WrappedSignedOpenAttestationVC, Salt }; From cd34a7d386cd5858de7a823e7a95ac27de0afe96 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Thu, 25 Apr 2024 12:04:59 +0800 Subject: [PATCH 20/43] refactor: better readability for wrapped types --- src/4.0/types.ts | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/4.0/types.ts b/src/4.0/types.ts index e1bf3c1c..19a27463 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -93,35 +93,21 @@ const WrappedSignedProof = WrappedProof.and( }) ); -const WrappedDocument = OpenAttestationVC.extend({ - proof: WrappedProof, -}); +const WrappedDocumentProofShape = { proof: WrappedProof } as const; +const WrappedDocument = OpenAttestationVC.extend(WrappedDocumentProofShape); -const SignedDocument = OpenAttestationVC.extend({ - proof: WrappedSignedProof, -}); +const SignedDocumentProofShape = { proof: WrappedSignedProof } as const; +const SignedDocument = OpenAttestationVC.extend(SignedDocumentProofShape); + +type WrappedOpenAttestationVC = Override< + T, + Pick, keyof typeof WrappedDocumentProofShape> +>; -type _WrappedDocument = z.infer; -type WrappedOpenAttestationVC< - T extends OpenAttestationVC = OpenAttestationVC, - U extends _WrappedDocument = Override< - T, - { - proof: z.infer; - } - > -> = U; - -type _SignedDocument = z.infer; -type WrappedSignedOpenAttestationVC< - T extends OpenAttestationVC = OpenAttestationVC, - U extends _SignedDocument = Override< - T, - { - proof: z.infer; - } - > -> = U; +type WrappedSignedOpenAttestationVC = Override< + T, + Pick, keyof typeof SignedDocumentProofShape> +>; /** Overrides properties in the Target (a & b does not override a props with b props) */ type Override, OverrideWith extends Record> = Omit< From d3baf69a18a009a1bad71c1acd49071c0c578d6d Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Thu, 25 Apr 2024 12:12:35 +0800 Subject: [PATCH 21/43] refactor: move things within file and some renaming --- src/4.0/types.ts | 49 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/4.0/types.ts b/src/4.0/types.ts index 19a27463..ec3d29e1 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -4,16 +4,13 @@ import { vcDataModel, zodUri } from "./validate/dataModel"; import { ContextUrl, ContextType } from "../shared/@types/document"; const IdentityProofType = z.union([z.literal("DNS-TXT"), z.literal("DNS-DID"), z.literal("DID")]); -type IdentityProofType = z.infer; - const Salt = z.object({ value: z.string(), path: z.string() }); -type Salt = z.infer; // Custom hex string validation function const HEX_STRING_REGEX = /^(0x)?[0-9a-fA-F]{40}$/; -const zodHexString = z.string().regex(HEX_STRING_REGEX, { message: "Invalid Hex String" }); +const HexString = z.string().regex(HEX_STRING_REGEX, { message: "Invalid Hex String" }); -const OpenAttestationVC = vcDataModel.extend({ +export const OpenAttestationVC = vcDataModel.extend({ "@context": z // Must be an array that starts with [baseContext, v4Context, ...] @@ -70,45 +67,49 @@ const OpenAttestationVC = vcDataModel.extend({ ) .optional(), }); -type VC = z.infer; -// AssertStricterOrEqual is used to ensure that we have zod extended from the base type while -// still being assignable to the base type. For example, if we accidentally extend and -// replaced '@context' to a boolean, this would fail the assertion. -type OpenAttestationVC = AssertStricterOrEqual>; const WrappedProof = z.object({ type: z.literal("OpenAttestationMerkleProofSignature2018"), proofPurpose: z.literal("assertionMethod"), - targetHash: zodHexString, - proofs: z.array(zodHexString), - merkleRoot: zodHexString, + targetHash: HexString, + proofs: z.array(HexString), + merkleRoot: HexString, salts: z.string(), - privacy: z.object({ obfuscated: z.array(zodHexString) }), + privacy: z.object({ obfuscated: z.array(HexString) }), }); +const WrappedDocumentExtrasShape = { proof: WrappedProof } as const; +const WrappedDocument = OpenAttestationVC.extend(WrappedDocumentExtrasShape); -const WrappedSignedProof = WrappedProof.and( +const SignedWrappedProof = WrappedProof.and( z.object({ key: z.string(), signature: z.string(), }) ); +const SignedWrappedDocumentExtrasShape = { proof: SignedWrappedProof } as const; +const SignedWrappedDocument = OpenAttestationVC.extend(SignedWrappedDocumentExtrasShape); -const WrappedDocumentProofShape = { proof: WrappedProof } as const; -const WrappedDocument = OpenAttestationVC.extend(WrappedDocumentProofShape); +export type VC = z.infer; -const SignedDocumentProofShape = { proof: WrappedSignedProof } as const; -const SignedDocument = OpenAttestationVC.extend(SignedDocumentProofShape); +// AssertStricterOrEqual is used to ensure that we have zod extended from the base type while +// still being assignable to the base type. For example, if we accidentally extend and +// replaced '@context' to a boolean, this would fail the assertion. +export type OpenAttestationVC = AssertStricterOrEqual>; -type WrappedOpenAttestationVC = Override< +export type WrappedOpenAttestationVC = Override< T, - Pick, keyof typeof WrappedDocumentProofShape> + Pick, keyof typeof WrappedDocumentExtrasShape> >; -type WrappedSignedOpenAttestationVC = Override< +export type WrappedSignedOpenAttestationVC = Override< T, - Pick, keyof typeof SignedDocumentProofShape> + Pick, keyof typeof SignedWrappedDocumentExtrasShape> >; +type IdentityProofType = z.infer; + +export type Salt = z.infer; + /** Overrides properties in the Target (a & b does not override a props with b props) */ type Override, OverrideWith extends Record> = Omit< Target, @@ -119,5 +120,3 @@ type Override, OverrideWith extends Recor /** Used to assert that StricterType is a stricter or equal version of LooserType, and most importantly, that * StricterType is STILL assignable to LooserType. */ type AssertStricterOrEqual = StricterType extends LooserType ? StricterType : never; - -export { VC, OpenAttestationVC, WrappedOpenAttestationVC, WrappedSignedOpenAttestationVC, Salt }; From 3bdc13e35e1ab8f493304e6a5d082a18f6d4a5fb Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Thu, 25 Apr 2024 12:29:57 +0800 Subject: [PATCH 22/43] refactor: improve naming of variables --- src/4.0/digest.ts | 4 ++-- src/4.0/obfuscate.ts | 12 ++++++------ src/4.0/sign.ts | 10 +++++----- src/4.0/types.ts | 22 +++++++++++----------- src/4.0/validate/dataModel.ts | 24 ++++++++++++------------ src/4.0/verify.ts | 4 ++-- src/4.0/wrap.ts | 24 +++++++++++------------- src/index.ts | 12 ++++++------ src/shared/utils/guard.ts | 10 +++------- src/shared/utils/utils.ts | 2 +- 10 files changed, 59 insertions(+), 65 deletions(-) diff --git a/src/4.0/digest.ts b/src/4.0/digest.ts index e171804f..1ce553b8 100644 --- a/src/4.0/digest.ts +++ b/src/4.0/digest.ts @@ -1,8 +1,8 @@ import { get, sortBy } from "lodash"; import { keccak256 } from "js-sha3"; -import { OpenAttestationVC, Salt } from "./types"; +import { V4Document, Salt } from "./types"; -export const digestCredential = (document: OpenAttestationVC, salts: Salt[], obfuscatedData: string[]) => { +export const digestCredential = (document: V4Document, salts: Salt[], obfuscatedData: string[]) => { // Prepare array of hashes from visible data const hashedUnhashedDataArray = salts // Explictly allow falsy values (e.g. false, 0, etc.) as they can exist in the document diff --git a/src/4.0/obfuscate.ts b/src/4.0/obfuscate.ts index 90caa0f3..56456869 100644 --- a/src/4.0/obfuscate.ts +++ b/src/4.0/obfuscate.ts @@ -2,9 +2,9 @@ import { toBuffer } from "../shared/utils"; import { cloneDeep, get, unset, pick } from "lodash"; import { decodeSalt, encodeSalt } from "./salt"; import { traverseAndFlatten } from "./traverseAndFlatten"; -import { OpenAttestationVC, WrappedOpenAttestationVC } from "./types"; +import { V4Document, V4WrappedDocument } from "./types"; -const obfuscate = (_data: WrappedOpenAttestationVC, fields: string[] | string) => { +const obfuscate = (_data: V4WrappedDocument, fields: string[] | string) => { const data = cloneDeep(_data); // Prevents alteration of original data const fieldsAsArray = ([] as string[]).concat(fields); @@ -36,10 +36,10 @@ const obfuscate = (_data: WrappedOpenAttestationVC, fields: string[] | string) = }; }; -export const obfuscateVerifiableCredential = ( - document: WrappedOpenAttestationVC, +export const obfuscateVerifiableCredential = ( + document: V4WrappedDocument, fields: string[] | string -): WrappedOpenAttestationVC => { +): V4WrappedDocument => { const { data, obfuscatedData } = obfuscate(document, fields); const currentObfuscatedData = document.proof.privacy.obfuscated; const newObfuscatedData = currentObfuscatedData.concat(obfuscatedData); @@ -52,5 +52,5 @@ export const obfuscateVerifiableCredential = ; + } satisfies V4WrappedDocument as V4WrappedDocument; }; diff --git a/src/4.0/sign.ts b/src/4.0/sign.ts index 31f7c0f5..3d3e6f3e 100644 --- a/src/4.0/sign.ts +++ b/src/4.0/sign.ts @@ -2,17 +2,17 @@ import { sign } from "../shared/signer"; import { SigningKey, SUPPORTED_SIGNING_ALGORITHM } from "../shared/@types/sign"; import { isSignedWrappedV4Document } from "../shared/utils"; import { ethers } from "ethers"; -import { OpenAttestationVC, WrappedOpenAttestationVC, WrappedSignedOpenAttestationVC } from "./types"; +import { V4Document, V4WrappedDocument, V4SignedWrappedDocument } from "./types"; -export const signDocument = async ( - document: WrappedSignedOpenAttestationVC | WrappedOpenAttestationVC, +export const signDocument = async ( + document: V4SignedWrappedDocument | V4WrappedDocument, algorithm: SUPPORTED_SIGNING_ALGORITHM, keyOrSigner: SigningKey | ethers.Signer -): Promise> => { +): Promise> => { if (isSignedWrappedV4Document(document)) throw new Error("Document has been signed"); const merkleRoot = `0x${document.proof.merkleRoot}`; const signature = await sign(algorithm, merkleRoot, keyOrSigner); - const proof: WrappedSignedOpenAttestationVC["proof"] = { + const proof: V4SignedWrappedDocument["proof"] = { ...document.proof, key: SigningKey.guard(keyOrSigner) ? keyOrSigner.public : `did:ethr:${await keyOrSigner.getAddress()}#controller`, signature, diff --git a/src/4.0/types.ts b/src/4.0/types.ts index ec3d29e1..1d806d4d 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -1,6 +1,6 @@ import z from "zod"; -import { vcDataModel, zodUri } from "./validate/dataModel"; +import { W3cVerifiableCredential, Uri } from "./validate/dataModel"; import { ContextUrl, ContextType } from "../shared/@types/document"; const IdentityProofType = z.union([z.literal("DNS-TXT"), z.literal("DNS-DID"), z.literal("DID")]); @@ -10,7 +10,7 @@ const Salt = z.object({ value: z.string(), path: z.string() }); const HEX_STRING_REGEX = /^(0x)?[0-9a-fA-F]{40}$/; const HexString = z.string().regex(HEX_STRING_REGEX, { message: "Invalid Hex String" }); -export const OpenAttestationVC = vcDataModel.extend({ +export const V4Document = W3cVerifiableCredential.extend({ "@context": z // Must be an array that starts with [baseContext, v4Context, ...] @@ -26,7 +26,7 @@ export const OpenAttestationVC = vcDataModel.extend({ issuer: z.object({ // Must have id match uri pattern - id: zodUri, + id: Uri, type: z.literal("OpenAttestationIssuer"), name: z.string(), identityProof: z.object({ @@ -78,7 +78,7 @@ const WrappedProof = z.object({ privacy: z.object({ obfuscated: z.array(HexString) }), }); const WrappedDocumentExtrasShape = { proof: WrappedProof } as const; -const WrappedDocument = OpenAttestationVC.extend(WrappedDocumentExtrasShape); +export const V4WrappedDocument = V4Document.extend(WrappedDocumentExtrasShape); const SignedWrappedProof = WrappedProof.and( z.object({ @@ -87,23 +87,23 @@ const SignedWrappedProof = WrappedProof.and( }) ); const SignedWrappedDocumentExtrasShape = { proof: SignedWrappedProof } as const; -const SignedWrappedDocument = OpenAttestationVC.extend(SignedWrappedDocumentExtrasShape); +export const V4SignedWrappedDocument = V4Document.extend(SignedWrappedDocumentExtrasShape); -export type VC = z.infer; +export type VC = z.infer; // AssertStricterOrEqual is used to ensure that we have zod extended from the base type while // still being assignable to the base type. For example, if we accidentally extend and // replaced '@context' to a boolean, this would fail the assertion. -export type OpenAttestationVC = AssertStricterOrEqual>; +export type V4Document = AssertStricterOrEqual>; -export type WrappedOpenAttestationVC = Override< +export type V4WrappedDocument = Override< T, - Pick, keyof typeof WrappedDocumentExtrasShape> + Pick, keyof typeof WrappedDocumentExtrasShape> >; -export type WrappedSignedOpenAttestationVC = Override< +export type V4SignedWrappedDocument = Override< T, - Pick, keyof typeof SignedWrappedDocumentExtrasShape> + Pick, keyof typeof SignedWrappedDocumentExtrasShape> >; type IdentityProofType = z.infer; diff --git a/src/4.0/validate/dataModel.ts b/src/4.0/validate/dataModel.ts index 94a7b743..b867abb2 100644 --- a/src/4.0/validate/dataModel.ts +++ b/src/4.0/validate/dataModel.ts @@ -7,9 +7,9 @@ const baseType = "VerifiableCredential"; // Custom URI validation function const URI_REGEX = /^(?=.)(?!https?:\/(?:$|[^/]))(?!https?:\/\/\/)(?!https?:[^/])(?:[a-zA-Z][a-zA-Z\d+-\.]*:(?:(?:\/\/(?:[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:]*@)?(?:\[(?:(?:(?:[\dA-Fa-f]{1,4}:){6}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|::(?:[\dA-Fa-f]{1,4}:){5}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){4}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,1}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){3}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,2}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){2}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,3}[\dA-Fa-f]{1,4})?::[\dA-Fa-f]{1,4}:(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,4}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,5}[\dA-Fa-f]{1,4})?::[\dA-Fa-f]{1,4}|(?:(?:[\dA-Fa-f]{1,4}:){0,6}[\dA-Fa-f]{1,4})?::)|v[\dA-Fa-f]+\.[\w-\.~!\$&'\(\)\*\+,;=:]+)\]|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])|[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=]{1,255})(?::\d*)?(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)|\/(?:[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]+(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)?|[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]+(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*|(?:\/\/\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)))(?:\?[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@\/\?]*(?=#|$))?(?:#[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@\/\?]*)?$/; -export const zodUri = z.string().regex(URI_REGEX, { message: "Invalid URI" }); +export const Uri = z.string().regex(URI_REGEX, { message: "Invalid URI" }); -export const vcDataModel = z.object({ +export const W3cVerifiableCredential = z.object({ "@context": z.union([ z.record(z.any()), z.string(), @@ -18,7 +18,7 @@ export const vcDataModel = z.object({ ]), // [Optional] If string: Must match uri pattern - id: zodUri.optional(), + id: Uri.optional(), type: z.union([ z.string(), @@ -30,13 +30,13 @@ export const vcDataModel = z.object({ .union([ // If object: Must have id match uri pattern and type defined z.object({ - id: zodUri, + id: Uri, type: z.string(), }), // If array: Every object must have id match uri pattern and type defined z.array( z.object({ - id: zodUri, + id: Uri, type: z.string(), }) ), @@ -45,10 +45,10 @@ export const vcDataModel = z.object({ issuer: z.union([ // If string: Must match uri pattern - zodUri, + Uri, // If object: Must have id match uri pattern z.object({ - id: zodUri, + id: Uri, }), ]), @@ -72,7 +72,7 @@ export const vcDataModel = z.object({ credentialStatus: z .object({ // If id is present, id must match uri pattern (credentialStatus.id is optional and can be undefined) - id: zodUri.optional(), + id: Uri.optional(), // Must have type defined type: z.string(), }) @@ -82,13 +82,13 @@ export const vcDataModel = z.object({ .union([ // If object: Must have type defined. If id is present, id must match uri pattern (termsOfUse.id is optional and can be undefined) z.object({ - id: zodUri.optional(), + id: Uri.optional(), type: z.string(), }), // If array: Every object must have type defined. If id is present, id must match uri pattern (termsOfUse.id is optional and can be undefined) z.array( z.object({ - id: zodUri.optional(), + id: Uri.optional(), type: z.string(), }) ), @@ -99,13 +99,13 @@ export const vcDataModel = z.object({ .union([ // If object: Must have type defined. If id is present, id must match uri pattern (evidence.id is optional and can be undefined) z.object({ - id: zodUri.optional(), + id: Uri.optional(), type: z.string(), }), // If array: Every object must have type defined. If id is present, id must match uri pattern (evidence.id is optional and can be undefined) z.array( z.object({ - id: zodUri.optional(), + id: Uri.optional(), type: z.string(), }) ), diff --git a/src/4.0/verify.ts b/src/4.0/verify.ts index 1f1a0046..1deaa011 100644 --- a/src/4.0/verify.ts +++ b/src/4.0/verify.ts @@ -1,9 +1,9 @@ -import { WrappedOpenAttestationVC } from "./types"; +import { V4WrappedDocument } from "./types"; import { digestCredential } from "./digest"; import { checkProof } from "../shared/merkle"; import { decodeSalt, salt } from "./salt"; -export const verify = (document: T): document is T => { +export const verify = (document: T): document is T => { if (!document.proof) { return false; } diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index fe4b2db8..a260f9f0 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -1,19 +1,17 @@ import { hashToBuffer, isStringArray } from "../shared/utils"; import { MerkleTree } from "../shared/merkle"; import { ContextType, ContextUrl } from "../shared/@types/document"; -import { OpenAttestationVC, VC, WrappedOpenAttestationVC } from "./types"; +import { V4Document, VC, V4WrappedDocument } from "./types"; import { digestCredential } from "../4.0/digest"; import { encodeSalt, salt } from "./salt"; -import { interpretContexts, vcDataModel } from "./validate"; +import { interpretContexts, W3cVerifiableCredential } from "./validate"; -export const wrapDocument = async ( - credential: T -): Promise> => { +export const wrapDocument = async (document: T): Promise> => { /* 1a. try OpenAttestation VC validation, since most user will be issuing oa v4*/ - const oav4context = await OpenAttestationVC.pick({ "@context": true }).safeParseAsync(credential); // Superficial check on user intention + const oav4context = await V4Document.pick({ "@context": true }).safeParseAsync(document); // Superficial check on user intention let rawDocument: VC | undefined; if (oav4context.success) { - const oav4 = await OpenAttestationVC.safeParseAsync(rawDocument); + const oav4 = await V4Document.safeParseAsync(rawDocument); if (!oav4.success) { throw new Error( `Input document does not conform to OpenAttestation v4.0 Data Model: ${JSON.stringify(oav4.error.issues)}` @@ -24,7 +22,7 @@ export const wrapDocument = async ( /* 1b. only if OA VC validation fail do we continue with W3C VC data model validation */ if (!rawDocument) { - const vc = await vcDataModel.safeParseAsync(credential); + const vc = await W3cVerifiableCredential.safeParseAsync(document); if (!vc.success) throw new Error( `Input document does not conform to Verifiable Credentials v2.0 Data Model: ${JSON.stringify(vc.error.issues)}` @@ -47,7 +45,7 @@ export const wrapDocument = async ( rawDocument["@context"].forEach((context) => contexts.add(context)); } REQUIRED_CONTEXTS.forEach((c) => contexts.delete(c)); - const finalContexts: OpenAttestationVC["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; + const finalContexts: V4Document["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; /* 4. Type validation */ // Ensure that required types are present and in the correct order @@ -60,7 +58,7 @@ export const wrapDocument = async ( types.forEach((type) => types.add(type)); } REQUIRED_TYPES.forEach((t) => types.delete(t)); - const finalTypes: OpenAttestationVC["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; + const finalTypes: V4Document["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; const documentReadyForWrapping = { ...rawDocument, @@ -78,7 +76,7 @@ export const wrapDocument = async ( const merkleTree = new MerkleTree(batchBuffers); const merkleRoot = merkleTree.getRoot().toString("hex"); const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer: Buffer) => buffer.toString("hex")); - const verifiableCredential: WrappedOpenAttestationVC = { + const verifiableCredential: V4WrappedDocument = { ...documentReadyForWrapping, proof: { type: "OpenAttestationMerkleProofSignature2018", @@ -93,7 +91,7 @@ export const wrapDocument = async ( }, }; - return verifiableCredential as WrappedOpenAttestationVC; + return verifiableCredential as V4WrappedDocument; }; function assertAsOaVcProps(obj: VC, keys: K[]) { @@ -101,5 +99,5 @@ function assertAsOaVcProps(obj: VC, keys: K[]) { Object.entries(obj).forEach(([k, v]) => { if (keys.includes(k as K)) temp[k] = v; }); - return temp as { [key in K]: OpenAttestationVC[key] }; + return temp as { [key in K]: V4Document[key] }; } diff --git a/src/index.ts b/src/index.ts index d68ef8fd..3c7d290e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,15 +60,15 @@ export function __unsafe__use__it__at__your__own__risks__wrapDocuments( +export function _unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocument( data: T -): Promise> { +): Promise> { return wrapDocumentV4(data); } -export function _unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocuments( +export function _unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocuments( dataArray: T[] -): Promise[]> { +): Promise[]> { return wrapDocumentsV4(dataArray); } @@ -93,7 +93,7 @@ export function verifySignature( algorithm: SUPPORTED_SIGNING_ALGORITHM, keyOrSigner: SigningKey | ethers.Signer ): Promise>; -export async function signDocument( +export async function signDocument( document: v4.SignedWrappedDocument | v4.WrappedDocument, algorithm: SUPPORTED_SIGNING_ALGORITHM, keyOrSigner: SigningKey | ethers.Signer diff --git a/src/shared/utils/guard.ts b/src/shared/utils/guard.ts index be2dea4d..508f7678 100644 --- a/src/shared/utils/guard.ts +++ b/src/shared/utils/guard.ts @@ -7,11 +7,7 @@ import { OpenAttestationDocument as OpenAttestationDocumentV3, WrappedDocument as WrappedDocumentV3, } from "../../3.0/types"; -import { - OpenAttestationVC as OpenAttestationDocumentV4, - WrappedOpenAttestationVC, - WrappedSignedOpenAttestationVC, -} from "../../4.0/types"; +import { V4Document as OpenAttestationDocumentV4, V4WrappedDocument, V4SignedWrappedDocument } from "../../4.0/types"; import { diagnose } from "./diagnose"; import { Mode } from "./@types/diagnose"; @@ -80,7 +76,7 @@ export const isWrappedV3Document = ( * @param document * @param mode strict or non-strict. Strict will perform additional check on the data. For instance strict validation will ensure that a target hash is a 32 bytes hex string while non strict validation will just check that target hash is a string. */ -export const isWrappedV4Document = ( +export const isWrappedV4Document = ( document: unknown, { mode }: { mode: Mode } = { mode: "non-strict" } ): document is T => { @@ -116,7 +112,7 @@ export const isSignedWrappedV3Document = ( * @param document * @param mode strict or non-strict. Strict will perform additional check on the data. For instance strict validation will ensure that a target hash is a 32 bytes hex string while non strict validation will just check that target hash is a string. */ -export const isSignedWrappedV4Document = ( +export const isSignedWrappedV4Document = ( document: unknown, { mode }: { mode: Mode } = { mode: "non-strict" } ): document is T => { diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index 5fb6248a..95eb96a6 100644 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -10,7 +10,7 @@ import * as v3 from "../../__generated__/schema.3.0"; import { WrappedDocument as WrappedDocumentV3 } from "../../3.0/types"; import { OpenAttestationDocument as OpenAttestationDocumentV3 } from "../../__generated__/schema.3.0"; -import { WrappedOpenAttestationVC as WrappedDocumentV4 } from "../../4.0/types"; +import { V4WrappedDocument as WrappedDocumentV4 } from "../../4.0/types"; import { OpenAttestationDocument, WrappedDocument, SchemaId, ContextUrl } from "../@types/document"; import { From db12f509a966119e89e1f08398b8821fbf97829e Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Thu, 25 Apr 2024 12:49:33 +0800 Subject: [PATCH 23/43] fix: use new typings --- .vscode/settings.json | 2 ++ src/4.0/__tests__/digest.test.ts | 4 ++-- src/shared/@types/document.ts | 16 ++++++---------- src/shared/utils/diagnose.ts | 5 ----- 4 files changed, 10 insertions(+), 17 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7a73a41b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/src/4.0/__tests__/digest.test.ts b/src/4.0/__tests__/digest.test.ts index 612e6d14..7d8609c5 100644 --- a/src/4.0/__tests__/digest.test.ts +++ b/src/4.0/__tests__/digest.test.ts @@ -1,12 +1,12 @@ import { cloneDeep } from "lodash"; import { digestCredential } from "../digest"; -import { WrappedDocument } from "../../4.0/types"; import { obfuscateVerifiableCredential } from "../obfuscate"; import { decodeSalt } from "../salt"; import sample from "../../../test/fixtures/v4/did-signed-wrapped.json"; +import { V4WrappedDocument } from "../types"; // TODO: remove unknown -const verifiableCredential = sample as unknown as WrappedDocument; +const verifiableCredential = sample as unknown as V4WrappedDocument; // Digest will change whenever sample document is regenerated const credentialRoot = "f49be3b06f7a7eb074775ad12aae43936084c86646e3640eae18e7aeca4f7468"; diff --git a/src/shared/@types/document.ts b/src/shared/@types/document.ts index 0d8c8bec..153dec84 100644 --- a/src/shared/@types/document.ts +++ b/src/shared/@types/document.ts @@ -1,7 +1,6 @@ // types generated by quicktype during postinstall phase import { OpenAttestationDocument as OpenAttestationDocumentV2 } from "../../__generated__/schema.2.0"; import { OpenAttestationDocument as OpenAttestationDocumentV3 } from "../../__generated__/schema.3.0"; -import { OpenAttestationDocument as OpenAttestationDocumentV4 } from "../../__generated__/schema.4.0"; import { SignedWrappedDocument as SignedWrappedDocumentV2, WrappedDocument as WrappedDocumentV2, @@ -10,27 +9,24 @@ import { SignedWrappedDocument as SignedWrappedDocumentV3, WrappedDocument as WrappedDocumentV3, } from "../../3.0/types"; -import { - SignedWrappedDocument as SignedWrappedDocumentV4, - WrappedDocument as WrappedDocumentV4, -} from "../../4.0/types"; import { Literal, Static, String } from "runtypes"; import { ethers } from "ethers"; +import { V4Document, V4SignedWrappedDocument, V4WrappedDocument } from "src/4.0/types"; -export type OpenAttestationDocument = OpenAttestationDocumentV2 | OpenAttestationDocumentV3 | OpenAttestationDocumentV4; +export type OpenAttestationDocument = OpenAttestationDocumentV2 | OpenAttestationDocumentV3 | V4Document; export type WrappedDocument = T extends OpenAttestationDocumentV2 ? WrappedDocumentV2 : T extends OpenAttestationDocumentV3 ? WrappedDocumentV3 - : T extends OpenAttestationDocumentV4 - ? WrappedDocumentV4 + : T extends V4Document + ? V4WrappedDocument : unknown; export type SignedWrappedDocument = T extends OpenAttestationDocumentV2 ? SignedWrappedDocumentV2 : T extends OpenAttestationDocumentV3 ? SignedWrappedDocumentV3 - : T extends OpenAttestationDocumentV4 - ? SignedWrappedDocumentV4 + : T extends V4Document + ? V4SignedWrappedDocument : unknown; export enum SchemaId { diff --git a/src/shared/utils/diagnose.ts b/src/shared/utils/diagnose.ts index bc857504..0b20a8f0 100644 --- a/src/shared/utils/diagnose.ts +++ b/src/shared/utils/diagnose.ts @@ -6,11 +6,6 @@ import { VerifiableCredentialWrappedProofStrict as WrappedProofStrictV3, VerifiableCredentialSignedProof as SignedWrappedProofV3, } from "../../3.0/types"; -import { - WrappedProof as WrappedProofV4, - WrappedProofStrict as WrappedProofStrictV4, - SignedWrappedProof as SignedWrappedProofV4, -} from "../../4.0/types"; import { ArrayProof, Signature, SignatureStrict } from "../../2.0/types"; import { clone, cloneDeepWith } from "lodash"; import { buildAjv, getSchema } from "../ajv"; From 22df1652b96a8211c0c6372551e278b1dd11b039 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 00:37:20 +0800 Subject: [PATCH 24/43] fix: passthroughs needed to allow extension, added name and render method so they dont get stripped --- src/4.0/validate/dataModel.ts | 85 ++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/src/4.0/validate/dataModel.ts b/src/4.0/validate/dataModel.ts index b867abb2..94304f25 100644 --- a/src/4.0/validate/dataModel.ts +++ b/src/4.0/validate/dataModel.ts @@ -17,6 +17,9 @@ export const W3cVerifiableCredential = z.object({ z.tuple([z.literal(ContextUrl.v2_vc)]).rest(z.union([z.string(), z.record(z.any())])), ]), + // [Optional] + name: z.string().optional(), + // [Optional] If string: Must match uri pattern id: Uri.optional(), @@ -29,16 +32,20 @@ export const W3cVerifiableCredential = z.object({ credentialSchema: z .union([ // If object: Must have id match uri pattern and type defined - z.object({ - id: Uri, - type: z.string(), - }), - // If array: Every object must have id match uri pattern and type defined - z.array( - z.object({ + z + .object({ id: Uri, type: z.string(), }) + .passthrough(), + // If array: Every object must have id match uri pattern and type defined + z.array( + z + .object({ + id: Uri, + type: z.string(), + }) + .passthrough() ), ]) .optional(), @@ -47,9 +54,11 @@ export const W3cVerifiableCredential = z.object({ // If string: Must match uri pattern Uri, // If object: Must have id match uri pattern - z.object({ - id: Uri, - }), + z + .object({ + id: Uri, + }) + .passthrough(), ]), validFrom: z.string().datetime({ offset: true }).optional(), @@ -76,21 +85,29 @@ export const W3cVerifiableCredential = z.object({ // Must have type defined type: z.string(), }) + .passthrough() .optional(), + // [Optional] This is at risk of being removed from the w3c spec + renderMethod: z.any().optional(), + termsOfUse: z .union([ // If object: Must have type defined. If id is present, id must match uri pattern (termsOfUse.id is optional and can be undefined) - z.object({ - id: Uri.optional(), - type: z.string(), - }), - // If array: Every object must have type defined. If id is present, id must match uri pattern (termsOfUse.id is optional and can be undefined) - z.array( - z.object({ + z + .object({ id: Uri.optional(), type: z.string(), }) + .passthrough(), + // If array: Every object must have type defined. If id is present, id must match uri pattern (termsOfUse.id is optional and can be undefined) + z.array( + z + .object({ + id: Uri.optional(), + type: z.string(), + }) + .passthrough() ), ]) .optional(), @@ -98,16 +115,20 @@ export const W3cVerifiableCredential = z.object({ evidence: z .union([ // If object: Must have type defined. If id is present, id must match uri pattern (evidence.id is optional and can be undefined) - z.object({ - id: Uri.optional(), - type: z.string(), - }), - // If array: Every object must have type defined. If id is present, id must match uri pattern (evidence.id is optional and can be undefined) - z.array( - z.object({ + z + .object({ id: Uri.optional(), type: z.string(), }) + .passthrough(), + // If array: Every object must have type defined. If id is present, id must match uri pattern (evidence.id is optional and can be undefined) + z.array( + z + .object({ + id: Uri.optional(), + type: z.string(), + }) + .passthrough() ), ]) .optional(), @@ -115,14 +136,18 @@ export const W3cVerifiableCredential = z.object({ proof: z .union([ // If object: Must have type defined - z.object({ - type: z.string(), - }), - // If array: Every object must have type defined - z.array( - z.object({ + z + .object({ type: z.string(), }) + .passthrough(), + // If array: Every object must have type defined + z.array( + z + .object({ + type: z.string(), + }) + .passthrough() ), ]) .optional(), From fc0ce61fec95619471220c388a9ad51d95e89dc3 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 00:37:37 +0800 Subject: [PATCH 25/43] test: added a guard test which is currently failing --- src/4.0/__tests__/guard.test.ts | 180 ++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/4.0/__tests__/guard.test.ts diff --git a/src/4.0/__tests__/guard.test.ts b/src/4.0/__tests__/guard.test.ts new file mode 100644 index 00000000..4660f1f5 --- /dev/null +++ b/src/4.0/__tests__/guard.test.ts @@ -0,0 +1,180 @@ +import { V4SignedWrappedDocument, VC, V4Document, V4WrappedDocument } from "../types"; +import { W3cVerifiableCredential } from "../validate"; + +const RAW_DOCUMENT: V4Document = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://schemata.openattestation.com/com/openattestation/4.0/alpha-context.json", + ], + type: ["VerifiableCredential", "OpenAttestationCredential"], + validFrom: "2021-03-08T12:00:00+08:00", + name: "Republic of Singapore Driving Licence", + issuer: { + id: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", + type: "OpenAttestationIssuer", + name: "Government Technology Agency of Singapore (GovTech)", + identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, + }, + renderMethod: [ + { + id: "https://demo-renderer.opencerts.io", + type: "OpenAttestationEmbeddedRenderer", + templateName: "GOVTECH_DEMO", + }, + ], + credentialSubject: { + id: "urn:uuid:a013fb9d-bb03-4056-b696-05575eceaf42", + type: ["DriversLicense"], + name: "John Doe", + licenses: [ + { + class: "3", + description: "Motor cars with unladen weight <= 3000kg", + effectiveDate: "2013-05-16T00:00:00+08:00", + }, + { + class: "3A", + description: "Motor cars with unladen weight <= 3000kg", + effectiveDate: "2013-05-16T00:00:00+08:00", + }, + ], + }, +}; + +const SIGNED_WRAPPED: V4SignedWrappedDocument = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://schemata.openattestation.com/com/openattestation/4.0/alpha-context.json", + ], + type: ["VerifiableCredential", "OpenAttestationCredential"], + validFrom: "2021-03-08T12:00:00+08:00", + name: "Republic of Singapore Driving Licence", + issuer: { + id: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", + type: "OpenAttestationIssuer", + name: "Government Technology Agency of Singapore (GovTech)", + identityProof: { identityProofType: "DNS-DID", identifier: "example.openattestation.com" }, + }, + renderMethod: [ + { + id: "https://demo-renderer.opencerts.io", + type: "OpenAttestationEmbeddedRenderer", + templateName: "GOVTECH_DEMO", + }, + ], + credentialSubject: { + id: "urn:uuid:a013fb9d-bb03-4056-b696-05575eceaf42", + type: ["DriversLicense"], + name: "John Doe", + licenses: [ + { + class: "3", + description: "Motor cars with unladen weight <= 3000kg", + effectiveDate: "2013-05-16T00:00:00+08:00", + }, + { + class: "3A", + description: "Motor cars with unladen weight <= 3000kg", + effectiveDate: "2013-05-16T00:00:00+08:00", + }, + ], + }, + proof: { + type: "OpenAttestationMerkleProofSignature2018", + proofPurpose: "assertionMethod", + targetHash: "f49be3b06f7a7eb074775ad12aae43936084c86646e3640eae18e7aeca4f7468", + proofs: [], + merkleRoot: "f49be3b06f7a7eb074775ad12aae43936084c86646e3640eae18e7aeca4f7468", + salts: + "W3sidmFsdWUiOiJiMzAzYWIyNmEyNjI1MGQ2YWNkMmI1Yzk0NmY3NDdhMTdkOTRlZTZmZjVhNDE2Mjk5OTQ4MDA0Y2EwNWE3MTBiIiwicGF0aCI6IkBjb250ZXh0WzBdIn0seyJ2YWx1ZSI6IjBiNGRmOGVhZDkwMzMwNjMyYjhkNWNkYWVjOGRkZTI0NzQ0NjFkMTE2NzgwNzU4OTRiMmUwY2JmZDQ1M2ZlNTUiLCJwYXRoIjoiQGNvbnRleHRbMV0ifSx7InZhbHVlIjoiZGE2NDE1MGViNzViYzY2NzdkYTkxYTFhMTk3YWUzMmYwMDBlN2M3OTEyN2Q2M2EzMWRiMzg5MWQ3YjQxNTYwOCIsInBhdGgiOiJ0eXBlWzBdIn0seyJ2YWx1ZSI6Ijc4MDhkZTQyZjdkMWZkNzE4ZDFhMmRhMDUzZDA4ZmQ2Y2JjOTliZDU3YzhmZTQyN2MxNzllZWQ1YmRlY2IyMTQiLCJwYXRoIjoidHlwZVsxXSJ9LHsidmFsdWUiOiI2ZDM1ODU2ZTRkMjg1MjllZmIyZjE2ZmRjMDFlMWE5MjQ5NDlhYzE5NzkwYjAxNmJkM2EyOGUxNjg0Mjc4NzNlIiwicGF0aCI6InZhbGlkRnJvbSJ9LHsidmFsdWUiOiI2NTQ5ZTkzOGU3MjgxMTM3NzZkOTViNTdhOTg1ZTY0MmFmYmNhZWMzOTg3ODhlZGIzNmZiZDMwZDY4ODMxZWRhIiwicGF0aCI6Im5hbWUifSx7InZhbHVlIjoiZDVhOTMxMTk5ZjVmZjBiM2MxZjQ3NjhlODNiNWNiODVmYmM0Y2Q4MDY2YmM3OTlkZTVlYWFmODViOWNhNjM4ZCIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiMTM3YmU4NzU2ZmU4YWUxMGI4ODI1ODQ5N2QyZjBkYmZlZDcwN2U5YTZlMzE1NDJiYjBiZGE3YjFhOTNhNmJmZCIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiI4NGUyOGZhMWM0MTQyYjg5M2ZiMTNkZjJjYTQyMTkxOGQwODEzNzNlYjc2ZTdiOWU5YTUwMzBmYzJhODBhNWZjIiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6IjdmYTQ0MDRlZTdhNTJjNGI3YWM3N2U1ODIwMDc2OTQ2YTU3MzNmMGJlNjIzOWJiZDUwNWZhYTY5MzVjNjkwMzUiLCJwYXRoIjoiaXNzdWVyLmlkZW50aXR5UHJvb2YuaWRlbnRpdHlQcm9vZlR5cGUifSx7InZhbHVlIjoiYzcxMGRjZDdjMjNmODhhYWY2ZDJjM2Q0ZDgxYzg3NzkzODcxYjI1NjI4MjJjN2YwNjliZmM4ZTA2ODdjNzRhYSIsInBhdGgiOiJpc3N1ZXIuaWRlbnRpdHlQcm9vZi5pZGVudGlmaWVyIn0seyJ2YWx1ZSI6ImI0NWU5ZjA5Mzg0NTVjMWY1YjhiMzJmN2E1MWZhMDkzOWYyYjg1ZmM4YWFkZDQzMzU2NjdlNzFjYzY2OGMwYmMiLCJwYXRoIjoicmVuZGVyTWV0aG9kWzBdLmlkIn0seyJ2YWx1ZSI6IjcyNDE3MDVlNTY5YjdmNjVlMmM1ZWZlMmYzZjIwYWVmMDdlMTdhZTIzNzI5NWRkYzJhYWM1MDAxZjI0YjAzNDkiLCJwYXRoIjoicmVuZGVyTWV0aG9kWzBdLnR5cGUifSx7InZhbHVlIjoiYTcyZDdlMmY3NTE4ZGZkYWY1N2JmMzI1Njg0YTNjY2Y3YmI2MDFkNjI0NGE0YzZmNDVhMzJmOTY5NjBiOGI1YiIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0udGVtcGxhdGVOYW1lIn0seyJ2YWx1ZSI6IjNhODI4MTc3MDQxZDI5MDk4NjkzNjhhZjQ3OGE2ZjJkYjU2MWIyYWQyZTY1YmYyNzlkNjE2MzAyNjc1MWJhMTciLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiODgyMWY4YzI3OTlkMDVjMmFmODBlZGZmYTc5ODQxMTFiZWM4Y2Y2ZTU1YWZiOWIxMWY3ZGE0YjU4NDE1MmRiYyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC50eXBlWzBdIn0seyJ2YWx1ZSI6IjBhMjUxOGExNzExMmE3YmY5OTY0M2FiYjc0YWI1ZTllZGViYmEzYzdlMGYzZDM5M2M5MGJjMGZiNDUzOWRmZmYiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubmFtZSJ9LHsidmFsdWUiOiIzMmZiN2JjM2NiZjFjMmZmYjcxYjQ2N2EzNWYyNTFmNGFiNzZkODA0MWUxYTNlMmQ4NzgwODc1NDBhZTQxYzIzIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzBdLmNsYXNzIn0seyJ2YWx1ZSI6ImZhNTRjNzRkMTYzNzMwZTNlOWRmNzYyMGRhMTllYjcxNjNjNGQyMDNiMDZhYTU0NzZmNzBmMzRiMDMzN2Y4MTIiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMF0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiNmUxMWE0MjkyMjEyNDdmNTJiOTU0ZjYxYzc2MTI3ZGYzZWYzY2E4NTA0ZmZlNGUyZDk1NWFjOWNmMjBmNDU3NiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1swXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6ImU3M2I3ZTNiM2E3OTA4MmQyNWU0OTA5YjU4MzdkZGFjNzRmZDA4ZjVlNjljOTcxZDJlYmViZGY5OWEyN2Q1MmYiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMV0uY2xhc3MifSx7InZhbHVlIjoiZjk1MzcxNWJjMzNlMTAzNzBjNGQ5MWUwMTZmN2M4MThjZWRjMGI1ZGRkMmZiODhmNGNiNGIzZTlhMzMwN2ZiMyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1sxXS5kZXNjcmlwdGlvbiJ9LHsidmFsdWUiOiJjM2NkZWNiZjNkOTgzZmUxNDRhNmI5NTJkZTY4ZmExYjUwZjUxOTQwZDgzMjY3MjQ5MTg1YWNmNTFiNmI2MDQ5IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzFdLmVmZmVjdGl2ZURhdGUifV0=", + privacy: { obfuscated: [] }, + key: "did:ethr:0xe93502ce1A52C1c0e99A2eB6666263EA53dB0a5e#controller", + signature: + "0x170fbb2d5916a7b3a4863feb8b705f5560c0b42311b164b2da32e682a8633b6f2c332f963db8267ab9a1c3be16ba1091388ed70e6e2a4ec240f5c0865557c6aa1c", + }, +}; + +describe("v4 guard", () => { + describe("given a raw document", () => { + test("should pass w3c vc validation without removal of any data", () => { + const w3cVerifiableCredential: VC = RAW_DOCUMENT; + const results = W3cVerifiableCredential.parse(w3cVerifiableCredential); + expect(results).toEqual(RAW_DOCUMENT); + }); + + test("should pass document validation without removal of any data", () => { + const results = V4Document.parse(RAW_DOCUMENT); + expect(results).toEqual(RAW_DOCUMENT); + }); + + test("should fail wrapped document validation", () => { + const results = V4WrappedDocument.safeParse(RAW_DOCUMENT); + expect(results.success).toBe(false); + expect((results as { error: unknown }).error).toMatchInlineSnapshot(` + [ZodError: [ + { + "code": "invalid_type", + "expected": "object", + "received": "undefined", + "path": [ + "proof" + ], + "message": "Required" + } + ]] + `); + }); + + test("should fail signed wrapped document validation", () => { + const results = V4SignedWrappedDocument.safeParse(RAW_DOCUMENT); + expect(results.success).toBe(false); + expect((results as { error: unknown }).error).toMatchInlineSnapshot(` + [ZodError: [ + { + "code": "invalid_type", + "expected": "object", + "received": "undefined", + "path": [ + "proof" + ], + "message": "Required" + }, + { + "code": "invalid_type", + "expected": "object", + "received": "undefined", + "path": [ + "proof" + ], + "message": "Required" + } + ]] + `); + }); + }); + + describe("given a signed wrapped document", () => { + test("should pass w3c vc validation without removal of any data", () => { + const w3cVerifiableCredential: VC = SIGNED_WRAPPED; + const results = W3cVerifiableCredential.parse(w3cVerifiableCredential); + expect(results).toEqual(SIGNED_WRAPPED); + }); + + test("should pass document validation without removal of any data", () => { + const v4Document: V4Document = SIGNED_WRAPPED; + const results = V4Document.parse(v4Document); + expect(results).toEqual(SIGNED_WRAPPED); + }); + + test("should pass wrapped document validation without removal of any data", () => { + const v4WrappedDocument: V4WrappedDocument = SIGNED_WRAPPED; + const results = V4WrappedDocument.parse(v4WrappedDocument); + expect(results).toEqual(SIGNED_WRAPPED); + }); + + test("should pass signed wrapped document validation without removal of any data", () => { + const results = V4SignedWrappedDocument.parse(SIGNED_WRAPPED); + expect(results).toEqual(SIGNED_WRAPPED); + }); + }); +}); From 8f421784b58425096b6e470a312b55c8cb2c513e Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 00:59:28 +0800 Subject: [PATCH 26/43] refactor: merged data model with types and renamed VC type --- src/4.0/__tests__/guard.test.ts | 7 +- src/4.0/types.ts | 157 +++++++++++++++++++++++++++++++- src/4.0/validate/dataModel.ts | 154 ------------------------------- src/4.0/validate/index.ts | 1 - src/4.0/wrap.ts | 23 +++-- 5 files changed, 172 insertions(+), 170 deletions(-) delete mode 100644 src/4.0/validate/dataModel.ts diff --git a/src/4.0/__tests__/guard.test.ts b/src/4.0/__tests__/guard.test.ts index 4660f1f5..110d3cde 100644 --- a/src/4.0/__tests__/guard.test.ts +++ b/src/4.0/__tests__/guard.test.ts @@ -1,5 +1,4 @@ -import { V4SignedWrappedDocument, VC, V4Document, V4WrappedDocument } from "../types"; -import { W3cVerifiableCredential } from "../validate"; +import { W3cVerifiableCredential, V4Document, V4WrappedDocument, V4SignedWrappedDocument } from "../types"; const RAW_DOCUMENT: V4Document = { "@context": [ @@ -97,7 +96,7 @@ const SIGNED_WRAPPED: V4SignedWrappedDocument = { describe("v4 guard", () => { describe("given a raw document", () => { test("should pass w3c vc validation without removal of any data", () => { - const w3cVerifiableCredential: VC = RAW_DOCUMENT; + const w3cVerifiableCredential: W3cVerifiableCredential = RAW_DOCUMENT; const results = W3cVerifiableCredential.parse(w3cVerifiableCredential); expect(results).toEqual(RAW_DOCUMENT); }); @@ -155,7 +154,7 @@ describe("v4 guard", () => { describe("given a signed wrapped document", () => { test("should pass w3c vc validation without removal of any data", () => { - const w3cVerifiableCredential: VC = SIGNED_WRAPPED; + const w3cVerifiableCredential: W3cVerifiableCredential = SIGNED_WRAPPED; const results = W3cVerifiableCredential.parse(w3cVerifiableCredential); expect(results).toEqual(SIGNED_WRAPPED); }); diff --git a/src/4.0/types.ts b/src/4.0/types.ts index 1d806d4d..7d0e809e 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -1,8 +1,157 @@ import z from "zod"; - -import { W3cVerifiableCredential, Uri } from "./validate/dataModel"; import { ContextUrl, ContextType } from "../shared/@types/document"; +const BASE_TYPE = "VerifiableCredential" as const; + +// Custom URI validation function +const URI_REGEX = + /^(?=.)(?!https?:\/(?:$|[^/]))(?!https?:\/\/\/)(?!https?:[^/])(?:[a-zA-Z][a-zA-Z\d+-\.]*:(?:(?:\/\/(?:[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:]*@)?(?:\[(?:(?:(?:[\dA-Fa-f]{1,4}:){6}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|::(?:[\dA-Fa-f]{1,4}:){5}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){4}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,1}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){3}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,2}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){2}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,3}[\dA-Fa-f]{1,4})?::[\dA-Fa-f]{1,4}:(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,4}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,5}[\dA-Fa-f]{1,4})?::[\dA-Fa-f]{1,4}|(?:(?:[\dA-Fa-f]{1,4}:){0,6}[\dA-Fa-f]{1,4})?::)|v[\dA-Fa-f]+\.[\w-\.~!\$&'\(\)\*\+,;=:]+)\]|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])|[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=]{1,255})(?::\d*)?(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)|\/(?:[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]+(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)?|[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]+(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*|(?:\/\/\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)))(?:\?[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@\/\?]*(?=#|$))?(?:#[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@\/\?]*)?$/; +const Uri = z.string().regex(URI_REGEX, { message: "Invalid URI" }); + +export const W3cVerifiableCredential = z.object({ + "@context": z.union([ + z.record(z.any()), + z.string(), + // If array: First item must be baseContext, while remaining items can be string or object + z.tuple([z.literal(ContextUrl.v2_vc)]).rest(z.union([z.string(), z.record(z.any())])), + ]), + + // [Optional] + name: z.string().optional(), + + // [Optional] If string: Must match uri pattern + id: Uri.optional(), + + type: z.union([ + z.string(), + // If array: Must have VerifiableCredential, while remaining items can be any string + z.array(z.string()).refine((types) => types.includes(BASE_TYPE), { message: `Type must include ${BASE_TYPE}` }), + ]), + + credentialSchema: z + .union([ + // If object: Must have id match uri pattern and type defined + z + .object({ + id: Uri, + type: z.string(), + }) + .passthrough(), + // If array: Every object must have id match uri pattern and type defined + z.array( + z + .object({ + id: Uri, + type: z.string(), + }) + .passthrough() + ), + ]) + .optional(), + + issuer: z.union([ + // If string: Must match uri pattern + Uri, + // If object: Must have id match uri pattern + z + .object({ + id: Uri, + }) + .passthrough(), + ]), + + validFrom: z.string().datetime({ offset: true }).optional(), + + validUntil: z.string().datetime({ offset: true }).optional(), + + credentialSubject: z.union([ + // If object: Cannot be empty (i.e. minimum 1 key) + z.record(z.any()).refine((obj) => Object.keys(obj).length > 0, { + message: "Must have at least one key", + }), + // If array: Every object cannot be empty (i.e. minimum 1 key) + z.array( + z.record(z.any()).refine((obj) => Object.keys(obj).length > 0, { + message: "Must have at least one key", + }) + ), + ]), + + credentialStatus: z + .object({ + // If id is present, id must match uri pattern (credentialStatus.id is optional and can be undefined) + id: Uri.optional(), + // Must have type defined + type: z.string(), + }) + .passthrough() + .optional(), + + // [Optional] This is at risk of being removed from the w3c spec + renderMethod: z.any().optional(), + + termsOfUse: z + .union([ + // If object: Must have type defined. If id is present, id must match uri pattern (termsOfUse.id is optional and can be undefined) + z + .object({ + id: Uri.optional(), + type: z.string(), + }) + .passthrough(), + // If array: Every object must have type defined. If id is present, id must match uri pattern (termsOfUse.id is optional and can be undefined) + z.array( + z + .object({ + id: Uri.optional(), + type: z.string(), + }) + .passthrough() + ), + ]) + .optional(), + + evidence: z + .union([ + // If object: Must have type defined. If id is present, id must match uri pattern (evidence.id is optional and can be undefined) + z + .object({ + id: Uri.optional(), + type: z.string(), + }) + .passthrough(), + // If array: Every object must have type defined. If id is present, id must match uri pattern (evidence.id is optional and can be undefined) + z.array( + z + .object({ + id: Uri.optional(), + type: z.string(), + }) + .passthrough() + ), + ]) + .optional(), + + proof: z + .union([ + // If object: Must have type defined + z + .object({ + type: z.string(), + }) + .passthrough(), + // If array: Every object must have type defined + z.array( + z + .object({ + type: z.string(), + }) + .passthrough() + ), + ]) + .optional(), +}); + const IdentityProofType = z.union([z.literal("DNS-TXT"), z.literal("DNS-DID"), z.literal("DID")]); const Salt = z.object({ value: z.string(), path: z.string() }); @@ -89,12 +238,12 @@ const SignedWrappedProof = WrappedProof.and( const SignedWrappedDocumentExtrasShape = { proof: SignedWrappedProof } as const; export const V4SignedWrappedDocument = V4Document.extend(SignedWrappedDocumentExtrasShape); -export type VC = z.infer; +export type W3cVerifiableCredential = z.infer; // AssertStricterOrEqual is used to ensure that we have zod extended from the base type while // still being assignable to the base type. For example, if we accidentally extend and // replaced '@context' to a boolean, this would fail the assertion. -export type V4Document = AssertStricterOrEqual>; +export type V4Document = AssertStricterOrEqual>; export type V4WrappedDocument = Override< T, diff --git a/src/4.0/validate/dataModel.ts b/src/4.0/validate/dataModel.ts deleted file mode 100644 index 94304f25..00000000 --- a/src/4.0/validate/dataModel.ts +++ /dev/null @@ -1,154 +0,0 @@ -import z from "zod"; - -import { ContextUrl } from "../../shared/@types/document"; - -const baseType = "VerifiableCredential"; - -// Custom URI validation function -const URI_REGEX = - /^(?=.)(?!https?:\/(?:$|[^/]))(?!https?:\/\/\/)(?!https?:[^/])(?:[a-zA-Z][a-zA-Z\d+-\.]*:(?:(?:\/\/(?:[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:]*@)?(?:\[(?:(?:(?:[\dA-Fa-f]{1,4}:){6}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|::(?:[\dA-Fa-f]{1,4}:){5}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){4}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,1}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){3}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,2}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:){2}(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,3}[\dA-Fa-f]{1,4})?::[\dA-Fa-f]{1,4}:(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,4}[\dA-Fa-f]{1,4})?::(?:[\dA-Fa-f]{1,4}:[\dA-Fa-f]{1,4}|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(?:(?:[\dA-Fa-f]{1,4}:){0,5}[\dA-Fa-f]{1,4})?::[\dA-Fa-f]{1,4}|(?:(?:[\dA-Fa-f]{1,4}:){0,6}[\dA-Fa-f]{1,4})?::)|v[\dA-Fa-f]+\.[\w-\.~!\$&'\(\)\*\+,;=:]+)\]|(?:(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:0{0,2}\d|0?[1-9]\d|1\d\d|2[0-4]\d|25[0-5])|[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=]{1,255})(?::\d*)?(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)|\/(?:[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]+(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)?|[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]+(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*|(?:\/\/\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*(?:\/[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@]*)*)))(?:\?[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@\/\?]*(?=#|$))?(?:#[\w-\.~%\dA-Fa-f!\$&'\(\)\*\+,;=:@\/\?]*)?$/; -export const Uri = z.string().regex(URI_REGEX, { message: "Invalid URI" }); - -export const W3cVerifiableCredential = z.object({ - "@context": z.union([ - z.record(z.any()), - z.string(), - // If array: First item must be baseContext, while remaining items can be string or object - z.tuple([z.literal(ContextUrl.v2_vc)]).rest(z.union([z.string(), z.record(z.any())])), - ]), - - // [Optional] - name: z.string().optional(), - - // [Optional] If string: Must match uri pattern - id: Uri.optional(), - - type: z.union([ - z.string(), - // If array: Must have VerifiableCredential, while remaining items can be any string - z.array(z.string()).refine((types) => types.includes(baseType), { message: `Type must include ${baseType}` }), - ]), - - credentialSchema: z - .union([ - // If object: Must have id match uri pattern and type defined - z - .object({ - id: Uri, - type: z.string(), - }) - .passthrough(), - // If array: Every object must have id match uri pattern and type defined - z.array( - z - .object({ - id: Uri, - type: z.string(), - }) - .passthrough() - ), - ]) - .optional(), - - issuer: z.union([ - // If string: Must match uri pattern - Uri, - // If object: Must have id match uri pattern - z - .object({ - id: Uri, - }) - .passthrough(), - ]), - - validFrom: z.string().datetime({ offset: true }).optional(), - - validUntil: z.string().datetime({ offset: true }).optional(), - - credentialSubject: z.union([ - // If object: Cannot be empty (i.e. minimum 1 key) - z.record(z.any()).refine((obj) => Object.keys(obj).length > 0, { - message: "Must have at least one key", - }), - // If array: Every object cannot be empty (i.e. minimum 1 key) - z.array( - z.record(z.any()).refine((obj) => Object.keys(obj).length > 0, { - message: "Must have at least one key", - }) - ), - ]), - - credentialStatus: z - .object({ - // If id is present, id must match uri pattern (credentialStatus.id is optional and can be undefined) - id: Uri.optional(), - // Must have type defined - type: z.string(), - }) - .passthrough() - .optional(), - - // [Optional] This is at risk of being removed from the w3c spec - renderMethod: z.any().optional(), - - termsOfUse: z - .union([ - // If object: Must have type defined. If id is present, id must match uri pattern (termsOfUse.id is optional and can be undefined) - z - .object({ - id: Uri.optional(), - type: z.string(), - }) - .passthrough(), - // If array: Every object must have type defined. If id is present, id must match uri pattern (termsOfUse.id is optional and can be undefined) - z.array( - z - .object({ - id: Uri.optional(), - type: z.string(), - }) - .passthrough() - ), - ]) - .optional(), - - evidence: z - .union([ - // If object: Must have type defined. If id is present, id must match uri pattern (evidence.id is optional and can be undefined) - z - .object({ - id: Uri.optional(), - type: z.string(), - }) - .passthrough(), - // If array: Every object must have type defined. If id is present, id must match uri pattern (evidence.id is optional and can be undefined) - z.array( - z - .object({ - id: Uri.optional(), - type: z.string(), - }) - .passthrough() - ), - ]) - .optional(), - - proof: z - .union([ - // If object: Must have type defined - z - .object({ - type: z.string(), - }) - .passthrough(), - // If array: Every object must have type defined - z.array( - z - .object({ - type: z.string(), - }) - .passthrough() - ), - ]) - .optional(), -}); diff --git a/src/4.0/validate/index.ts b/src/4.0/validate/index.ts index a29ffd44..2edd280c 100644 --- a/src/4.0/validate/index.ts +++ b/src/4.0/validate/index.ts @@ -1,2 +1 @@ export * from "./context"; -export * from "./dataModel"; diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index a260f9f0..b1437f1d 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -1,15 +1,15 @@ import { hashToBuffer, isStringArray } from "../shared/utils"; import { MerkleTree } from "../shared/merkle"; import { ContextType, ContextUrl } from "../shared/@types/document"; -import { V4Document, VC, V4WrappedDocument } from "./types"; +import { V4Document, V4WrappedDocument, W3cVerifiableCredential } from "./types"; import { digestCredential } from "../4.0/digest"; import { encodeSalt, salt } from "./salt"; -import { interpretContexts, W3cVerifiableCredential } from "./validate"; +import { interpretContexts } from "./validate"; export const wrapDocument = async (document: T): Promise> => { /* 1a. try OpenAttestation VC validation, since most user will be issuing oa v4*/ const oav4context = await V4Document.pick({ "@context": true }).safeParseAsync(document); // Superficial check on user intention - let rawDocument: VC | undefined; + let rawDocument: W3cVerifiableCredential | undefined; if (oav4context.success) { const oav4 = await V4Document.safeParseAsync(rawDocument); if (!oav4.success) { @@ -62,10 +62,10 @@ export const wrapDocument = async (document: T): Promise(document: T): Promise; }; -function assertAsOaVcProps(obj: VC, keys: K[]) { +/** Extract a set of properties from w3cVerifiableCredential but only include the ones + * that are defined in the original document. For example, if we extract + * "a" and "b" from { b: "something" } we should only get { b: "something" } NOT + * { a: undefined, b: "something" }. We also assert that the extracted properties + * are of V4Document type. + **/ +function assertAsV4DocumentProps( + original: W3cVerifiableCredential, + keys: K[] +) { const temp: Record = {}; - Object.entries(obj).forEach(([k, v]) => { + Object.entries(original).forEach(([k, v]) => { if (keys.includes(k as K)) temp[k] = v; }); return temp as { [key in K]: V4Document[key] }; From 31872374d83f4aa36aad9000424ca060c82f0015 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 01:00:47 +0800 Subject: [PATCH 27/43] refactor: improve helper fn naming --- src/4.0/wrap.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index b1437f1d..c263f316 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -62,7 +62,7 @@ export const wrapDocument = async (document: T): Promise(document: T): Promise( +function extractAndAssertAsV4DocumentProps( original: W3cVerifiableCredential, keys: K[] ) { From 0f3d311cec30bb780bd73e9ba3cbef494b74444b Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 12:41:12 +0800 Subject: [PATCH 28/43] fix: use extend instead of and since it results in dup errors, remove hex validation since these props are generated by us, allow passthrough for wrap proof so that a signed wrap can pass a wrap only validation test --- src/4.0/types.ts | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/4.0/types.ts b/src/4.0/types.ts index 7d0e809e..b42d157e 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -155,10 +155,6 @@ export const W3cVerifiableCredential = z.object({ const IdentityProofType = z.union([z.literal("DNS-TXT"), z.literal("DNS-DID"), z.literal("DID")]); const Salt = z.object({ value: z.string(), path: z.string() }); -// Custom hex string validation function -const HEX_STRING_REGEX = /^(0x)?[0-9a-fA-F]{40}$/; -const HexString = z.string().regex(HEX_STRING_REGEX, { message: "Invalid Hex String" }); - export const V4Document = W3cVerifiableCredential.extend({ "@context": z @@ -220,21 +216,16 @@ export const V4Document = W3cVerifiableCredential.extend({ const WrappedProof = z.object({ type: z.literal("OpenAttestationMerkleProofSignature2018"), proofPurpose: z.literal("assertionMethod"), - targetHash: HexString, - proofs: z.array(HexString), - merkleRoot: HexString, + targetHash: z.string(), + proofs: z.array(z.string()), + merkleRoot: z.string(), salts: z.string(), - privacy: z.object({ obfuscated: z.array(HexString) }), + privacy: z.object({ obfuscated: z.array(z.string()) }), }); -const WrappedDocumentExtrasShape = { proof: WrappedProof } as const; +const WrappedDocumentExtrasShape = { proof: WrappedProof.passthrough() } as const; export const V4WrappedDocument = V4Document.extend(WrappedDocumentExtrasShape); -const SignedWrappedProof = WrappedProof.and( - z.object({ - key: z.string(), - signature: z.string(), - }) -); +const SignedWrappedProof = WrappedProof.extend({ key: z.string(), signature: z.string() }); const SignedWrappedDocumentExtrasShape = { proof: SignedWrappedProof } as const; export const V4SignedWrappedDocument = V4Document.extend(SignedWrappedDocumentExtrasShape); From 3c40ebc3d56613fa34d132062b84ebff93a8bc69 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 12:41:22 +0800 Subject: [PATCH 29/43] test: update snapshot --- src/4.0/__tests__/guard.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/4.0/__tests__/guard.test.ts b/src/4.0/__tests__/guard.test.ts index 110d3cde..f16712e8 100644 --- a/src/4.0/__tests__/guard.test.ts +++ b/src/4.0/__tests__/guard.test.ts @@ -129,15 +129,6 @@ describe("v4 guard", () => { expect(results.success).toBe(false); expect((results as { error: unknown }).error).toMatchInlineSnapshot(` [ZodError: [ - { - "code": "invalid_type", - "expected": "object", - "received": "undefined", - "path": [ - "proof" - ], - "message": "Required" - }, { "code": "invalid_type", "expected": "object", From 946ae6325ed281a6f8635b68e458280608f2b8be Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 12:54:05 +0800 Subject: [PATCH 30/43] fix: was using the wrong document for the check --- src/4.0/wrap.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index c263f316..4666eec0 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -9,40 +9,40 @@ import { interpretContexts } from "./validate"; export const wrapDocument = async (document: T): Promise> => { /* 1a. try OpenAttestation VC validation, since most user will be issuing oa v4*/ const oav4context = await V4Document.pick({ "@context": true }).safeParseAsync(document); // Superficial check on user intention - let rawDocument: W3cVerifiableCredential | undefined; + let validatedRawDocument: W3cVerifiableCredential | undefined; if (oav4context.success) { - const oav4 = await V4Document.safeParseAsync(rawDocument); + const oav4 = await V4Document.safeParseAsync(document); if (!oav4.success) { throw new Error( `Input document does not conform to OpenAttestation v4.0 Data Model: ${JSON.stringify(oav4.error.issues)}` ); } - rawDocument = oav4.data; + validatedRawDocument = oav4.data; } /* 1b. only if OA VC validation fail do we continue with W3C VC data model validation */ - if (!rawDocument) { + if (!validatedRawDocument) { const vc = await W3cVerifiableCredential.safeParseAsync(document); if (!vc.success) throw new Error( `Input document does not conform to Verifiable Credentials v2.0 Data Model: ${JSON.stringify(vc.error.issues)}` ); - rawDocument = vc.data; + validatedRawDocument = vc.data; } /* 2. Ensure provided @context are interpretable (e.g. valid @context URL, all types are mapped, etc.) */ - await interpretContexts(rawDocument); + await interpretContexts(validatedRawDocument); /* 3. Context validation */ // Ensure that required contexts are present and in the correct order // type: [Base, OA, ...] const REQUIRED_CONTEXTS = [ContextUrl.v2_vc, ContextUrl.v4_alpha] as const; const contexts = new Set(REQUIRED_CONTEXTS); - if (typeof rawDocument["@context"] === "string") { - contexts.add(rawDocument["@context"]); - } else if (isStringArray(rawDocument["@context"])) { - rawDocument["@context"].forEach((context) => contexts.add(context)); + if (typeof validatedRawDocument["@context"] === "string") { + contexts.add(validatedRawDocument["@context"]); + } else if (isStringArray(validatedRawDocument["@context"])) { + validatedRawDocument["@context"].forEach((context) => contexts.add(context)); } REQUIRED_CONTEXTS.forEach((c) => contexts.delete(c)); const finalContexts: V4Document["@context"] = [...REQUIRED_CONTEXTS, ...Array.from(contexts)]; @@ -52,17 +52,17 @@ export const wrapDocument = async (document: T): Promise([ContextType.BaseContext, ContextType.V4AlphaContext]); - if (typeof rawDocument["type"] === "string") { - types.add(rawDocument["type"]); - } else if (isStringArray(rawDocument["type"])) { + if (typeof validatedRawDocument["type"] === "string") { + types.add(validatedRawDocument["type"]); + } else if (isStringArray(validatedRawDocument["type"])) { types.forEach((type) => types.add(type)); } REQUIRED_TYPES.forEach((t) => types.delete(t)); const finalTypes: V4Document["type"] = [...REQUIRED_TYPES, ...Array.from(types)]; const documentReadyForWrapping = { - ...rawDocument, - ...extractAndAssertAsV4DocumentProps(rawDocument, ["issuer", "credentialStatus"]), + ...validatedRawDocument, + ...extractAndAssertAsV4DocumentProps(validatedRawDocument, ["issuer", "credentialStatus"]), "@context": finalContexts, type: finalTypes, } satisfies W3cVerifiableCredential; From d8c46940bf18005de5a3ffabd2a5c9dd765440e4 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 12:59:47 +0800 Subject: [PATCH 31/43] test: added tests for wrapped only document --- src/4.0/__tests__/guard.test.ts | 90 +++++++++++++++++++++++++++++++++ src/shared/ajv.ts | 2 - src/shared/utils/diagnose.ts | 8 +-- 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/src/4.0/__tests__/guard.test.ts b/src/4.0/__tests__/guard.test.ts index f16712e8..c40f81c3 100644 --- a/src/4.0/__tests__/guard.test.ts +++ b/src/4.0/__tests__/guard.test.ts @@ -40,6 +40,48 @@ const RAW_DOCUMENT: V4Document = { }, }; +const WRAPPED: V4WrappedDocument = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://schemata.openattestation.com/com/openattestation/4.0/alpha-context.json", + ], + name: "Republic of Singapore Driving Licence", + type: ["VerifiableCredential", "OpenAttestationCredential"], + issuer: { + id: "did:ethr:0xB26B4941941C51a4885E5B7D3A1B861E54405f90", + type: "OpenAttestationIssuer", + name: "Government Technology Agency of Singapore (GovTech)", + identityProof: { + identityProofType: "DNS-DID", + identifier: "example.openattestation.com", + }, + }, + validFrom: "2021-03-08T12:00:00+08:00", + credentialSubject: { + id: "urn:uuid:a013fb9d-bb03-4056-b696-05575eceaf42", + type: ["DriversLicense"], + name: "John Doe", + licenses: [[Object], [Object]], + }, + renderMethod: [ + { + id: "https://demo-renderer.opencerts.io", + type: "OpenAttestationEmbeddedRenderer", + templateName: "GOVTECH_DEMO", + }, + ], + proof: { + type: "OpenAttestationMerkleProofSignature2018", + proofPurpose: "assertionMethod", + targetHash: "5746a5bc0a8aa0cfdaa6ab14bd63d10a91713b0d8450f0403d86f777ff4ba81b", + proofs: [], + merkleRoot: "5746a5bc0a8aa0cfdaa6ab14bd63d10a91713b0d8450f0403d86f777ff4ba81b", + salts: + "W3sidmFsdWUiOiI3ZDgzZGNkMWU2NjM1ZjAxZWM3NjZlNzQwZDM1ZTE4MjU2OWM3MjdjNjViNDZlMTQyMDY3NDRiMTFhMThhYWNiIiwicGF0aCI6IkBjb250ZXh0WzBdIn0seyJ2YWx1ZSI6IjI3OGJmZjY2ZTA4N2VhNDBkNjcwZTQ0OTgwYzUyZWM4NTRlOWViZjEyYjM2M2VhZTEyNDExOWQwMzgyMTgxMzUiLCJwYXRoIjoiQGNvbnRleHRbMV0ifSx7InZhbHVlIjoiY2U0YTJhOWMzMjBhOTU1MDlmNGQwODIwZGE1N2M3OWVlMDE1ZTQyNzgwODg3ZDkzZjE4ODIwMGNhMDNmMGI1OCIsInBhdGgiOiJuYW1lIn0seyJ2YWx1ZSI6ImEzMGVmNzM1MTljZmNjMDE3Zjk5MzBkYWJmNjk2YmNjMjY1ZjI4MDljZjBlOGQyNmUwMzUzYjY3NWZiMzMyZjYiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiIyMzczMzYwMGNjOTBjMjQ3MzhjOThjMWQxMTJiY2ExZDc0NWFkODMyNjczY2RlMWE5ZjhmYWM3MWZmN2FkODQyIiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiZjhhZDY4MmVjMGMzYzJjMjZlZjhmY2FkNDg1NzE4ODNiMTU3M2ZkMjYzZTA4N2NlMWZmNDQ3ZGUxYjAxYWRiMCIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiMzIxMzEwYTI4MDMwZjE1NmEzM2VkNzM5N2MzNGE0ZTk2OTkxNjVkZTQyNjRiODgyMGVmNTZkMGJiOWQ1NDM1MyIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiI3NzM1NGIxMTQ4MTA4ZDIzYWQ5OTI5NDE4MmViYmJmNzM1NzIyNjg4YTdjOGFmYTQxOTM4ZGVkNzUwY2VlZTQzIiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6IjVhZjY1OTQ1YTlkNTEyODUwM2RjMTM3ZDgwM2ZmMzg5OTJhNTQ4MTg3ZjRlZjJkNThlMGQ4MjMwYjE5ZDZlZWMiLCJwYXRoIjoiaXNzdWVyLmlkZW50aXR5UHJvb2YuaWRlbnRpdHlQcm9vZlR5cGUifSx7InZhbHVlIjoiN2IxOTE4NjJiNWY0OWQxY2M1NTA0OWI0NGI2ODk4YmQwNThhMjlkNzg5MjljZDZjYjk2MWNiODgwOWE5ZjBmZSIsInBhdGgiOiJpc3N1ZXIuaWRlbnRpdHlQcm9vZi5pZGVudGlmaWVyIn0seyJ2YWx1ZSI6ImI5OGQxMTI0Y2RhYTU2NGY4YjJhZjE1OTE0OWM0MDBiODM4NWZhN2YxZThhYTRjNmExODEzNzg0NWUwMDIyMDEiLCJwYXRoIjoidmFsaWRGcm9tIn0seyJ2YWx1ZSI6ImFkYTY5ZDZlYjI3OWVjNjk0ZGE1ODMxZDU1N2Q3NjIxYWM4NWQwYzJlN2IxNDdmM2E5ZGNkMjM0NzA3MWZjMzkiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiMDdiYjI0NDRkZmQxMWVkY2M5OWI1MWJhMzllNTI4ODM3NWY2YTExY2U5YTg4Y2MxMjZkNmY4YjYxMzM2ZmQ0NiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC50eXBlWzBdIn0seyJ2YWx1ZSI6ImZlZGIyMDRmNWM4ZWM1OGU3NzUxYTNhNWM3YmU2MmQxZjhkYjg2MTAyOTI1ZGU1MzcyY2E3NGNlZWVlZjhiOWUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubmFtZSJ9LHsidmFsdWUiOiJjYTM4YjM3MDk0OTM5ZTE0MDNhN2RlNGUzNzljYTAzMzIzNTQ4ZTdmZjc2YmQ1MGI1ZmI0MGY4NjdmYzRkZjk2IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzBdLmNsYXNzIn0seyJ2YWx1ZSI6IjAxY2NkZjAxMTI3YWRiODAzYjJhNmQ2YmVjZWU2YTk4N2EyNzM1ZWIxYjYzZGI0Njg5NDMyYmY2NzNmMzIwYzYiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMF0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiZDJlZGY0NmZiMjQwNzRjNzY0Y2I4MzliMjYyNGNmNjQ5NWU2NTY5NzA1YmI3ZmMyZjllZjYxNDc0ZTdkODBjYSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1swXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6ImY4OWQyOWEwOGE5NmI2YjYyNWU5NjM0OWFiMGVlM2JmNDUzODZhODAxMDhiMTE2NTUwMjk0ODg3MmE4ZmExYmYiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMV0uY2xhc3MifSx7InZhbHVlIjoiMDdmY2JmMzhjYWYxOTYyZjI5MTBjNjk1ZGY1OTVlOWYzYzUwNjk2OWRlNDVlOWU5NWNhNzAwYjFiN2E1OGZlMSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1sxXS5kZXNjcmlwdGlvbiJ9LHsidmFsdWUiOiIyYzYyNGQ0NTJmNWE1MGJiMzlkMzY0NDMwMDA5NDYyMDY2NmY2NTk5MzJiODhmMmFjZWNjYzRiYzQwMmQ4MmNlIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzFdLmVmZmVjdGl2ZURhdGUifSx7InZhbHVlIjoiZmE5NWEyOGRhNjRlMjA4OTBiZWVjMTZiNGY2N2E2ZDMyNThkYThjYTljN2M1MDljNTZjYjMwZjg0NGUzMjNjOCIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0uaWQifSx7InZhbHVlIjoiYTZmM2FlMmYyNjM0OTkxNzg2NzZlZmMzZmVlYjc3OWEwM2IyMDlhOTQwN2Q1NWU1MDNlN2U4YTA5OWExYmFiMSIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0udHlwZSJ9LHsidmFsdWUiOiIyYjEyNGIzOGE0MDEyOWM1ZTM3OTZhZjQ3ZTEyZDg4YWI2YWZmNjllNzZiMTgyYWJmNTZiYjQ2OWU1MTZlNjc1IiwicGF0aCI6InJlbmRlck1ldGhvZFswXS50ZW1wbGF0ZU5hbWUifV0=", + privacy: { obfuscated: [] }, + }, +}; + const SIGNED_WRAPPED: V4SignedWrappedDocument = { "@context": [ "https://www.w3.org/ns/credentials/v2", @@ -143,6 +185,54 @@ describe("v4 guard", () => { }); }); + describe("given a wrapped document", () => { + test("should pass w3c vc validation without removal of any data", () => { + const w3cVerifiableCredential: W3cVerifiableCredential = WRAPPED; + const results = W3cVerifiableCredential.parse(w3cVerifiableCredential); + expect(results).toEqual(WRAPPED); + }); + + test("should pass document validation without removal of any data", () => { + const v4Document: V4Document = WRAPPED; + const results = V4Document.parse(v4Document); + expect(results).toEqual(WRAPPED); + }); + + test("should pass wrapped document validation without removal of any data", () => { + const results = V4WrappedDocument.parse(WRAPPED); + expect(results).toEqual(WRAPPED); + }); + + test("should fail signed wrapped document validation", () => { + const results = V4SignedWrappedDocument.safeParse(WRAPPED); + expect(results.success).toBe(false); + expect((results as { error: unknown }).error).toMatchInlineSnapshot(` + [ZodError: [ + { + "code": "invalid_type", + "expected": "string", + "received": "undefined", + "path": [ + "proof", + "key" + ], + "message": "Required" + }, + { + "code": "invalid_type", + "expected": "string", + "received": "undefined", + "path": [ + "proof", + "signature" + ], + "message": "Required" + } + ]] + `); + }); + }); + describe("given a signed wrapped document", () => { test("should pass w3c vc validation without removal of any data", () => { const w3cVerifiableCredential: W3cVerifiableCredential = SIGNED_WRAPPED; diff --git a/src/shared/ajv.ts b/src/shared/ajv.ts index 53879c1a..81e9405c 100644 --- a/src/shared/ajv.ts +++ b/src/shared/ajv.ts @@ -2,7 +2,6 @@ import Ajv from "ajv"; import addFormats from "ajv-formats"; import openAttestationSchemav2 from "../2.0/schema/schema.json"; import openAttestationSchemav3 from "../3.0/schema/schema.json"; -import openAttestationSchemav4 from "../4.0/schema/schema.json"; import { CurrentOptions } from "ajv/dist/core"; const defaultTransform = (schema: Record) => schema; @@ -17,7 +16,6 @@ export const buildAjv = ( ajv.addKeyword("deprecationMessage"); ajv.compile(transform(openAttestationSchemav2)); ajv.compile(transform(openAttestationSchemav3)); - ajv.compile(transform(openAttestationSchemav4)); return ajv; }; diff --git a/src/shared/utils/diagnose.ts b/src/shared/utils/diagnose.ts index 0b20a8f0..2dc42814 100644 --- a/src/shared/utils/diagnose.ts +++ b/src/shared/utils/diagnose.ts @@ -88,7 +88,7 @@ export const diagnose = ({ const versionToSchemaId: Record = { "2.0": SchemaId.v2, "3.0": SchemaId.v3, - "4.0": SchemaId.v4, + "4.0": "" as SchemaId, }; const errors = validate( @@ -252,9 +252,9 @@ const diagnoseV4 = ({ // 4. Check proof object // eslint-disable-next-line @typescript-eslint/no-unused-expressions if (mode === "strict") { - WrappedProofStrictV4.check(document.proof); + // WrappedProofStrictV4.check(document.proof); } else { - WrappedProofV4.check(document.proof); + // WrappedProofV4.check(document.proof); } } catch (e) { if (e instanceof Error) { @@ -270,7 +270,7 @@ const diagnoseV4 = ({ return handleError(debug, `The document does not have a proof`); } try { - SignedWrappedProofV4.check(document.proof); + // SignedWrappedProofV4.check(document.proof); } catch (e) { if (e instanceof Error) { return handleError(debug, e.message); From c5a5c9619a34c1485ae429a965342e7f5c373020 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 14:08:35 +0800 Subject: [PATCH 32/43] fix: diagnose to use new guards --- src/4.0/diagnose.ts | 22 +++++ src/shared/utils/@types/diagnose.ts | 3 + src/shared/utils/diagnose.ts | 130 ++++------------------------ 3 files changed, 41 insertions(+), 114 deletions(-) create mode 100644 src/4.0/diagnose.ts diff --git a/src/4.0/diagnose.ts b/src/4.0/diagnose.ts new file mode 100644 index 00000000..9984a53a --- /dev/null +++ b/src/4.0/diagnose.ts @@ -0,0 +1,22 @@ +import { Diagnose } from "src/shared/utils/@types/diagnose"; +import { V4WrappedDocument, V4SignedWrappedDocument } from "./types"; + +export const v4Diagnose: Diagnose = ({ document, kind, debug }) => { + const Validator = kind === "signed" ? V4SignedWrappedDocument : V4WrappedDocument; + + const results = Validator.safeParse(document); + + if (results.success) { + return []; + } + + return results.error.errors.map(({ code, message, path }) => { + const errorMessage = `${code}: ${message} at ${path.join(".")}`; + if (debug) { + console.debug(errorMessage); + } + return { + message: errorMessage, + }; + }); +}; diff --git a/src/shared/utils/@types/diagnose.ts b/src/shared/utils/@types/diagnose.ts index 91fb12af..0cd4e4f7 100644 --- a/src/shared/utils/@types/diagnose.ts +++ b/src/shared/utils/@types/diagnose.ts @@ -1,2 +1,5 @@ export type Kind = "raw" | "wrapped" | "signed"; export type Mode = "strict" | "non-strict"; +export type Diagnose = (props: { kind: Exclude; document: any; debug: boolean; mode: Mode }) => { + message: string; +}[]; diff --git a/src/shared/utils/diagnose.ts b/src/shared/utils/diagnose.ts index 2dc42814..11c83d62 100644 --- a/src/shared/utils/diagnose.ts +++ b/src/shared/utils/diagnose.ts @@ -1,5 +1,5 @@ import { logger } from "ethers"; -import { ContextUrl, SchemaId } from "../@types/document"; +import { SchemaId } from "../@types/document"; import { validateSchema as validate } from "../validate"; import { VerifiableCredentialWrappedProof as WrappedProofV3, @@ -10,7 +10,7 @@ import { ArrayProof, Signature, SignatureStrict } from "../../2.0/types"; import { clone, cloneDeepWith } from "lodash"; import { buildAjv, getSchema } from "../ajv"; import { Kind, Mode } from "./@types/diagnose"; -import { isStringArray } from "./utils"; +import { v4Diagnose } from "src/4.0/diagnose"; type Version = "2.0" | "3.0" | "4.0"; @@ -85,25 +85,24 @@ export const diagnose = ({ return handleError(debug, "The document must be an object"); } - const versionToSchemaId: Record = { + const versionToSchemaId: Record = { "2.0": SchemaId.v2, "3.0": SchemaId.v3, - "4.0": "" as SchemaId, + "4.0": undefined, }; - const errors = validate( - document, - getSchema(versionToSchemaId[version], mode === "non-strict" ? ajv : undefined), - kind - ); + const schemaId = versionToSchemaId[version]; + if (schemaId) { + const errors = validate(document, getSchema(schemaId, mode === "non-strict" ? ajv : undefined), kind); - if (errors.length > 0) { - // TODO this can be improved later - return handleError( - debug, - `The document does not match OpenAttestation schema ${version}`, - ...errors.map((error) => `${error.instancePath || "document"} - ${error.message}`) - ); + if (errors.length > 0) { + // TODO this can be improved later + return handleError( + debug, + `The document does not match OpenAttestation schema ${version}`, + ...errors.map((error) => `${error.instancePath || "document"} - ${error.message}`) + ); + } } if (kind === "raw") { @@ -112,7 +111,7 @@ export const diagnose = ({ switch (version) { case "4.0": - return diagnoseV4({ mode, debug, document, kind }); + return v4Diagnose({ mode, debug, document, kind }); case "3.0": return diagnoseV3({ mode, debug, document, kind }); case "2.0": @@ -185,100 +184,3 @@ const diagnoseV3 = ({ kind, document, debug, mode }: { kind: Kind; document: any } return []; }; - -const diagnoseV4 = ({ - kind, - document, - debug, - mode, -}: { - kind: Exclude; - document: any; - debug: boolean; - mode: Mode; -}) => { - /* Wrapped document checks */ - try { - // 1. Since OA v4 has deprecated a few properties from v2/v3, check that they are not used - const deprecatedProperties = ["version", "openAttestationMetadata"]; - const documentProperties = Object.keys(document); - const deprecatedDocumentProperties = documentProperties.filter((p) => deprecatedProperties.includes(p)); - - if (deprecatedDocumentProperties.length > 0) { - return handleError( - debug, - `The document has outdated properties previously used in v2/v3. The following properties are no longer in use in a v4 document: ${deprecatedDocumentProperties}` - ); - } - - // 2. Ensure that required @contexts are present - // @context: [Base, OA, ...] - const contexts = [ContextUrl.v2_vc, ContextUrl.v4_alpha]; - if (isStringArray(document["@context"])) { - for (let i = 0; i < contexts.length; i++) { - if (document["@context"][i] !== contexts[i]) { - return handleError( - debug, - `The document @context contains an unexpected value or in the wrong order. Expected "${contexts}" but received "${document["@context"]}"` - ); - } - } - } else { - return handleError( - debug, - `The document @context should be an array of string values. Expected "${contexts}" but received "${document["@context"]}"` - ); - } - - // 3. Ensure that required types are present - // type: ["VerifiableCredential", "OpenAttestationCredential", ...] - const types = ["VerifiableCredential", "OpenAttestationCredential"]; - if (isStringArray(document["type"])) { - for (let i = 0; i < types.length; i++) { - if (document["type"][i] !== types[i]) { - return handleError( - debug, - `The document type contains an unexpected value or in the wrong order. Expected "${types}" but received "${document["type"]}"` - ); - } - } - } else { - return handleError( - debug, - `The document type should be an array of string values. Expected "${types}" but received "${document["type"]}"` - ); - } - - // 4. Check proof object - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - if (mode === "strict") { - // WrappedProofStrictV4.check(document.proof); - } else { - // WrappedProofV4.check(document.proof); - } - } catch (e) { - if (e instanceof Error) { - return handleError(debug, e.message); - } else { - console.error(e); - } - } - - /* Signed & wrapped document checks */ - if (kind === "signed") { - if (!document.proof) { - return handleError(debug, `The document does not have a proof`); - } - try { - // SignedWrappedProofV4.check(document.proof); - } catch (e) { - if (e instanceof Error) { - return handleError(debug, e.message); - } else { - console.error(e); - } - } - } - - return []; -}; From 9561fbeda5e6224c6114e63df597bca7623edbb2 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 14:11:45 +0800 Subject: [PATCH 33/43] fix: path --- src/shared/utils/diagnose.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/utils/diagnose.ts b/src/shared/utils/diagnose.ts index 11c83d62..ef697584 100644 --- a/src/shared/utils/diagnose.ts +++ b/src/shared/utils/diagnose.ts @@ -10,7 +10,7 @@ import { ArrayProof, Signature, SignatureStrict } from "../../2.0/types"; import { clone, cloneDeepWith } from "lodash"; import { buildAjv, getSchema } from "../ajv"; import { Kind, Mode } from "./@types/diagnose"; -import { v4Diagnose } from "src/4.0/diagnose"; +import { v4Diagnose } from "../../4.0/diagnose"; type Version = "2.0" | "3.0" | "4.0"; From aa66692901796a635ae1349767980377a353a5bc Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 16:26:38 +0800 Subject: [PATCH 34/43] chore: remove vscode --- .vscode/settings.json | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7a73a41b..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file From 9b1900e0b7684f7dd63a2d12995e13ba0b2fc531 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 16:27:20 +0800 Subject: [PATCH 35/43] chore: ignore vscode settings --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e1632f13..1ff9e852 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ yarn.lock *.iml /public /vc-test-suite +/.vscode \ No newline at end of file From ff57b1c5e3efc0fd9fcc7ba093b44a12fecb476a Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 16:29:53 +0800 Subject: [PATCH 36/43] chore: remove experimental version guard --- src/shared/utils/diagnose.ts | 2 +- src/shared/utils/utils.ts | 16 +++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/shared/utils/diagnose.ts b/src/shared/utils/diagnose.ts index ef697584..97d8d5e7 100644 --- a/src/shared/utils/diagnose.ts +++ b/src/shared/utils/diagnose.ts @@ -12,7 +12,7 @@ import { buildAjv, getSchema } from "../ajv"; import { Kind, Mode } from "./@types/diagnose"; import { v4Diagnose } from "../../4.0/diagnose"; -type Version = "2.0" | "3.0" | "4.0"; +export type Version = "2.0" | "3.0" | "4.0"; interface DiagnoseError { message: string; diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index 95eb96a6..f82014c4 100644 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -21,13 +21,7 @@ import { isRawV4Document, isWrappedV4Document, } from "./guard"; - -const VersionGuard = { - 2: isWrappedV2Document, - 3: isWrappedV3Document, - 4: isWrappedV4Document, -} as const satisfies Record boolean>; -type OAVersion = keyof typeof VersionGuard; +import { Version } from "./diagnose"; export type Hash = string | Buffer; type Extract

= P extends WrappedDocumentV2 ? T : never; @@ -274,18 +268,18 @@ export const getObfuscatedData = ( export const isStringArray = (input: unknown): input is string[] => Array.isArray(input) && input.every((i) => typeof i === "string"); -export const getVersion = (document: unknown): OAVersion => { +export const getVersion = (document: unknown): Version => { if (typeof document === "object" && document !== null) { if ("version" in document && typeof document.version === "string") { switch (document.version) { case SchemaId.v2: - return 2; + return "2.0"; case SchemaId.v3: - return 3; + return "3.0"; } } else if ("@context" in document && Array.isArray(document["@context"])) { if (document["@context"].includes(ContextUrl.v4_alpha)) { - return 4; + return "4.0"; } } } From 6245038de0c2628a2d1686d5ba63486f20a8b5bc Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 16:44:55 +0800 Subject: [PATCH 37/43] fix: add back the missing wrapDocuments that was accidentally removed --- src/4.0/wrap.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index 4666eec0..07237b39 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -94,6 +94,32 @@ export const wrapDocument = async (document: T): Promise; }; +export const wrapDocuments = async (documents: T[]): Promise[]> => { + // create individual verifiable credential + const verifiableCredentials = await Promise.all(documents.map((document) => wrapDocument(document))); + + // get all the target hashes to compute the merkle tree and the merkle root + const merkleTree = new MerkleTree( + verifiableCredentials.map((verifiableCredential) => verifiableCredential.proof.targetHash).map(hashToBuffer) + ); + const merkleRoot = merkleTree.getRoot().toString("hex"); + + // for each document, update the merkle root and add the proofs needed + return verifiableCredentials.map((verifiableCredential) => { + const digest = verifiableCredential.proof.targetHash; + const merkleProof = merkleTree.getProof(hashToBuffer(digest)).map((buffer: Buffer) => buffer.toString("hex")); + + return { + ...verifiableCredential, + proof: { + ...verifiableCredential.proof, + proofs: merkleProof, + merkleRoot, + }, + }; + }); +}; + /** Extract a set of properties from w3cVerifiableCredential but only include the ones * that are defined in the original document. For example, if we extract * "a" and "b" from { b: "something" } we should only get { b: "something" } NOT From 4d965a4a043f03488b9b8eb5a0b6729a76f941eb Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 16:47:11 +0800 Subject: [PATCH 38/43] fix: replace back original implementation of getMerkleRoot --- src/shared/utils/utils.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index f82014c4..8cceffff 100644 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -100,20 +100,13 @@ export function getIssuerAddress(document: any): any { } export const getMerkleRoot = (document: any): string => { - const version = getVersion(document); - const getMerkleRoot: Record unknown | undefined> = { - 2: () => document?.signature?.merkleRoot, - 3: () => document?.proof?.merkleRoot, - 4: () => document?.proof?.merkleRoot, - }; - - const merkleRoot = getMerkleRoot[version](); - if (merkleRoot === undefined || typeof merkleRoot !== "string") - throw new Error( - "Unsupported document type: Only can retrieve merkle root from wrapped OpenAttestation v2, v3 & v4 documents." - ); - - return merkleRoot; + if (isWrappedV2Document(document)) return document.signature.merkleRoot; + else if (isWrappedV3Document(document)) return document.proof.merkleRoot; + else if (isWrappedV4Document(document)) return document.proof.merkleRoot; + + throw new Error( + "Unsupported document type: Only can retrieve merkle root from wrapped OpenAttestation v2, v3 & v4 documents." + ); }; export const getTargetHash = (document: any): string => { From 5b5ecea7fb54940baa98b257945bca367db00679 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 16:51:54 +0800 Subject: [PATCH 39/43] fix: make our own real types stricter --- src/4.0/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/4.0/types.ts b/src/4.0/types.ts index b42d157e..6f853a47 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -211,7 +211,7 @@ export const V4Document = W3cVerifiableCredential.extend({ ]) ) .optional(), -}); +}).strict(); const WrappedProof = z.object({ type: z.literal("OpenAttestationMerkleProofSignature2018"), @@ -223,11 +223,11 @@ const WrappedProof = z.object({ privacy: z.object({ obfuscated: z.array(z.string()) }), }); const WrappedDocumentExtrasShape = { proof: WrappedProof.passthrough() } as const; -export const V4WrappedDocument = V4Document.extend(WrappedDocumentExtrasShape); +export const V4WrappedDocument = V4Document.extend(WrappedDocumentExtrasShape).strict(); const SignedWrappedProof = WrappedProof.extend({ key: z.string(), signature: z.string() }); const SignedWrappedDocumentExtrasShape = { proof: SignedWrappedProof } as const; -export const V4SignedWrappedDocument = V4Document.extend(SignedWrappedDocumentExtrasShape); +export const V4SignedWrappedDocument = V4Document.extend(SignedWrappedDocumentExtrasShape).strict(); export type W3cVerifiableCredential = z.infer; From a99584a78d2213abca791f50de0312c34cc87012 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 17:39:56 +0800 Subject: [PATCH 40/43] fix: diagnose did not handle raw v4 --- src/4.0/diagnose.ts | 11 ++++++-- src/index.ts | 39 +++++++++++------------------ src/shared/utils/@types/diagnose.ts | 2 +- src/shared/utils/diagnose.ts | 4 +-- src/shared/utils/guard.ts | 16 ++++++------ 5 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/4.0/diagnose.ts b/src/4.0/diagnose.ts index 9984a53a..068b1de6 100644 --- a/src/4.0/diagnose.ts +++ b/src/4.0/diagnose.ts @@ -1,8 +1,15 @@ import { Diagnose } from "src/shared/utils/@types/diagnose"; -import { V4WrappedDocument, V4SignedWrappedDocument } from "./types"; +import { V4WrappedDocument, V4SignedWrappedDocument, V4Document } from "./types"; export const v4Diagnose: Diagnose = ({ document, kind, debug }) => { - const Validator = kind === "signed" ? V4SignedWrappedDocument : V4WrappedDocument; + let Validator: typeof V4Document | typeof V4WrappedDocument | typeof V4SignedWrappedDocument = V4Document; + if (kind === "raw") { + Validator = V4Document; + } else if (kind === "wrapped") { + Validator = V4WrappedDocument; + } else { + Validator = V4SignedWrappedDocument; + } const results = Validator.safeParse(document); diff --git a/src/index.ts b/src/index.ts index 3c7d290e..7abcce74 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import { getSchema } from "./shared/ajv"; import * as utils from "./shared/utils"; import { SchemaValidationError } from "./shared/utils"; import { validateSchema as validate } from "./shared/validate"; -import { SchemaId, WrappedDocument, OpenAttestationDocument } from "./shared/@types/document"; +import { SchemaId, WrappedDocument, OpenAttestationDocument, SignedWrappedDocument } from "./shared/@types/document"; import { WrapDocumentOptionV2, WrapDocumentOptionV3 } from "./shared/@types/wrap"; import { SigningKey, SUPPORTED_SIGNING_ALGORITHM } from "./shared/@types/sign"; @@ -31,6 +31,7 @@ import { signDocument as signDocumentV4 } from "./4.0/sign"; import { verify as verifyV4 } from "./4.0/verify"; import { digestCredential as digestCredentialV4 } from "./4.0/digest"; import { obfuscateVerifiableCredential as obfuscateVerifiableCredentialV4 } from "./4.0/obfuscate"; +import { v4Diagnose } from "./4.0/diagnose"; export function wrapDocument( data: T, @@ -78,7 +79,7 @@ export const validateSchema = (document: WrappedDocument): boolean => { else if (utils.isWrappedV3Document(document) || document?.version === SchemaId.v3) return validate(document, getSchema(SchemaId.v3)).length === 0; else if (utils.isWrappedV4Document(document)) { - return validate(document, getSchema(SchemaId.v4)).length === 0; + return v4Diagnose({ document, kind: "wrapped", debug: false, mode: "strict" }).length === 0; } return validate(document, getSchema(`${document?.version || SchemaId.v2}`)).length === 0; @@ -95,7 +96,7 @@ export function verifySignature( document: WrappedDocument, fields: string[] | string ): WrappedDocument; -export function obfuscate( +export function obfuscate( document: WrappedDocument, fields: string[] | string ): WrappedDocument; @@ -131,34 +132,22 @@ export const isSchemaValidationError = (error: any): error is SchemaValidationEr return !!error.validationErrors; }; -export async function signDocument( - document: v2.SignedWrappedDocument | v2.WrappedDocument, +export async function signDocument( + document: WrappedDocument | SignedWrappedDocument, algorithm: SUPPORTED_SIGNING_ALGORITHM, keyOrSigner: SigningKey | ethers.Signer -): Promise>; -export async function signDocument( - document: v3.SignedWrappedDocument | v3.WrappedDocument, - algorithm: SUPPORTED_SIGNING_ALGORITHM, - keyOrSigner: SigningKey | ethers.Signer -): Promise>; -export async function signDocument( - document: v4.SignedWrappedDocument | v4.WrappedDocument, - algorithm: SUPPORTED_SIGNING_ALGORITHM, - keyOrSigner: SigningKey | ethers.Signer -): Promise>; -export async function signDocument( - document: any, - algorithm: SUPPORTED_SIGNING_ALGORITHM, - keyOrSigner: SigningKey | ethers.Signer -) { +): Promise> { // rj was worried it could happen deep in the code, so I moved it to the boundaries if (!SigningKey.guard(keyOrSigner) && !Signer.isSigner(keyOrSigner)) { throw new Error(`Either a keypair or ethers.js Signer must be provided`); } - if (utils.isWrappedV2Document(document)) return signDocumentV2(document, algorithm, keyOrSigner); - else if (utils.isWrappedV3Document(document)) return signDocumentV3(document, algorithm, keyOrSigner); - else if (utils.isWrappedV4Document(document)) return signDocumentV4(document, algorithm, keyOrSigner); + let results: unknown; + if (utils.isWrappedV2Document(document)) results = signDocumentV2(document, algorithm, keyOrSigner); + else if (utils.isWrappedV3Document(document)) results = signDocumentV3(document, algorithm, keyOrSigner); + else if (utils.isWrappedV4Document(document)) results = signDocumentV4(document, algorithm, keyOrSigner); + + if (results) return results as SignedWrappedDocument; // Unreachable code atm until utils.isWrappedV2Document & utils.isWrappedV3Document becomes more strict throw new Error("Unsupported document type: Only OpenAttestation v2, v3 or v4 documents can be signed"); diff --git a/src/shared/utils/@types/diagnose.ts b/src/shared/utils/@types/diagnose.ts index 0cd4e4f7..dfcdcf7e 100644 --- a/src/shared/utils/@types/diagnose.ts +++ b/src/shared/utils/@types/diagnose.ts @@ -1,5 +1,5 @@ export type Kind = "raw" | "wrapped" | "signed"; export type Mode = "strict" | "non-strict"; -export type Diagnose = (props: { kind: Exclude; document: any; debug: boolean; mode: Mode }) => { +export type Diagnose = (props: { kind: Kind; document: any; debug: boolean; mode?: Mode }) => { message: string; }[]; diff --git a/src/shared/utils/diagnose.ts b/src/shared/utils/diagnose.ts index 97d8d5e7..919553ff 100644 --- a/src/shared/utils/diagnose.ts +++ b/src/shared/utils/diagnose.ts @@ -105,13 +105,13 @@ export const diagnose = ({ } } - if (kind === "raw") { + if (kind === "raw" && version !== "4.0") { return []; } switch (version) { case "4.0": - return v4Diagnose({ mode, debug, document, kind }); + return v4Diagnose({ debug, document, kind }); case "3.0": return diagnoseV3({ mode, debug, document, kind }); case "2.0": diff --git a/src/shared/utils/guard.ts b/src/shared/utils/guard.ts index 508f7678..dfe8e136 100644 --- a/src/shared/utils/guard.ts +++ b/src/shared/utils/guard.ts @@ -7,7 +7,7 @@ import { OpenAttestationDocument as OpenAttestationDocumentV3, WrappedDocument as WrappedDocumentV3, } from "../../3.0/types"; -import { V4Document as OpenAttestationDocumentV4, V4WrappedDocument, V4SignedWrappedDocument } from "../../4.0/types"; +import { V4WrappedDocument, V4SignedWrappedDocument, V4Document } from "../../4.0/types"; import { diagnose } from "./diagnose"; import { Mode } from "./@types/diagnose"; @@ -43,7 +43,7 @@ export const isRawV3Document = ( export const isRawV4Document = ( document: any, { mode }: { mode: Mode } = { mode: "non-strict" } -): document is OpenAttestationDocumentV4 => { +): document is V4Document => { return diagnose({ version: "4.0", kind: "raw", document, debug: false, mode }).length === 0; }; @@ -52,10 +52,10 @@ export const isRawV4Document = ( * @param document * @param mode strict or non-strict. Strict will perform additional check on the data. For instance strict validation will ensure that a target hash is a 32 bytes hex string while non strict validation will just check that target hash is a string. */ -export const isWrappedV2Document = ( +export const isWrappedV2Document = ( document: any, { mode }: { mode: Mode } = { mode: "non-strict" } -): document is WrappedDocumentV2 => { +): document is WrappedDocumentV2 => { return diagnose({ version: "2.0", kind: "wrapped", document, debug: false, mode }).length === 0; }; @@ -64,10 +64,10 @@ export const isWrappedV2Document = ( * @param document * @param mode strict or non-strict. Strict will perform additional check on the data. For instance strict validation will ensure that a target hash is a 32 bytes hex string while non strict validation will just check that target hash is a string. */ -export const isWrappedV3Document = ( +export const isWrappedV3Document = ( document: any, { mode }: { mode: Mode } = { mode: "non-strict" } -): document is WrappedDocumentV3 => { +): document is WrappedDocumentV3 => { return diagnose({ version: "3.0", kind: "wrapped", document, debug: false, mode }).length === 0; }; @@ -76,10 +76,10 @@ export const isWrappedV3Document = ( * @param document * @param mode strict or non-strict. Strict will perform additional check on the data. For instance strict validation will ensure that a target hash is a 32 bytes hex string while non strict validation will just check that target hash is a string. */ -export const isWrappedV4Document = ( +export const isWrappedV4Document = ( document: unknown, { mode }: { mode: Mode } = { mode: "non-strict" } -): document is T => { +): document is V4WrappedDocument => { return diagnose({ version: "4.0", kind: "wrapped", document, debug: false, mode }).length === 0; }; From a8219ee0acfcaa9aa80954c24240f47ff646dda6 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 17:41:53 +0800 Subject: [PATCH 41/43] refactor: remove useless renaming --- src/shared/utils/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index 8cceffff..d992735a 100644 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -10,7 +10,7 @@ import * as v3 from "../../__generated__/schema.3.0"; import { WrappedDocument as WrappedDocumentV3 } from "../../3.0/types"; import { OpenAttestationDocument as OpenAttestationDocumentV3 } from "../../__generated__/schema.3.0"; -import { V4WrappedDocument as WrappedDocumentV4 } from "../../4.0/types"; +import { V4WrappedDocument } from "../../4.0/types"; import { OpenAttestationDocument, WrappedDocument, SchemaId, ContextUrl } from "../@types/document"; import { @@ -224,7 +224,7 @@ export const isObfuscated = ( document: | WrappedDocumentV2 | WrappedDocumentV3 - | WrappedDocumentV4 + | V4WrappedDocument ): boolean => { if (isWrappedV2Document(document)) { return !!document.privacy?.obfuscatedData?.length; @@ -243,7 +243,7 @@ export const getObfuscatedData = ( document: | WrappedDocumentV2 | WrappedDocumentV3 - | WrappedDocumentV4 + | V4WrappedDocument ): string[] => { if (isWrappedV2Document(document)) { return document.privacy?.obfuscatedData || []; From 26fa2653d857d6955cf84c779191393a9e9c837d Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 18:08:07 +0800 Subject: [PATCH 42/43] fix: object object in test case --- src/4.0/__tests__/guard.test.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/4.0/__tests__/guard.test.ts b/src/4.0/__tests__/guard.test.ts index c40f81c3..07be75b8 100644 --- a/src/4.0/__tests__/guard.test.ts +++ b/src/4.0/__tests__/guard.test.ts @@ -61,7 +61,18 @@ const WRAPPED: V4WrappedDocument = { id: "urn:uuid:a013fb9d-bb03-4056-b696-05575eceaf42", type: ["DriversLicense"], name: "John Doe", - licenses: [[Object], [Object]], + licenses: [ + { + class: "3", + description: "Motor cars with unladen weight <= 3000kg", + effectiveDate: "2013-05-16T00:00:00+08:00", + }, + { + class: "3A", + description: "Motor cars with unladen weight <= 3000kg", + effectiveDate: "2013-05-16T00:00:00+08:00", + }, + ], }, renderMethod: [ { @@ -73,12 +84,14 @@ const WRAPPED: V4WrappedDocument = { proof: { type: "OpenAttestationMerkleProofSignature2018", proofPurpose: "assertionMethod", - targetHash: "5746a5bc0a8aa0cfdaa6ab14bd63d10a91713b0d8450f0403d86f777ff4ba81b", + targetHash: "f065a97f2ec23ff1469dedbcf9e41916e2a5e46b001e512d2d27d10ee87d8433", proofs: [], - merkleRoot: "5746a5bc0a8aa0cfdaa6ab14bd63d10a91713b0d8450f0403d86f777ff4ba81b", + merkleRoot: "f065a97f2ec23ff1469dedbcf9e41916e2a5e46b001e512d2d27d10ee87d8433", salts: - "W3sidmFsdWUiOiI3ZDgzZGNkMWU2NjM1ZjAxZWM3NjZlNzQwZDM1ZTE4MjU2OWM3MjdjNjViNDZlMTQyMDY3NDRiMTFhMThhYWNiIiwicGF0aCI6IkBjb250ZXh0WzBdIn0seyJ2YWx1ZSI6IjI3OGJmZjY2ZTA4N2VhNDBkNjcwZTQ0OTgwYzUyZWM4NTRlOWViZjEyYjM2M2VhZTEyNDExOWQwMzgyMTgxMzUiLCJwYXRoIjoiQGNvbnRleHRbMV0ifSx7InZhbHVlIjoiY2U0YTJhOWMzMjBhOTU1MDlmNGQwODIwZGE1N2M3OWVlMDE1ZTQyNzgwODg3ZDkzZjE4ODIwMGNhMDNmMGI1OCIsInBhdGgiOiJuYW1lIn0seyJ2YWx1ZSI6ImEzMGVmNzM1MTljZmNjMDE3Zjk5MzBkYWJmNjk2YmNjMjY1ZjI4MDljZjBlOGQyNmUwMzUzYjY3NWZiMzMyZjYiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiIyMzczMzYwMGNjOTBjMjQ3MzhjOThjMWQxMTJiY2ExZDc0NWFkODMyNjczY2RlMWE5ZjhmYWM3MWZmN2FkODQyIiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiZjhhZDY4MmVjMGMzYzJjMjZlZjhmY2FkNDg1NzE4ODNiMTU3M2ZkMjYzZTA4N2NlMWZmNDQ3ZGUxYjAxYWRiMCIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiMzIxMzEwYTI4MDMwZjE1NmEzM2VkNzM5N2MzNGE0ZTk2OTkxNjVkZTQyNjRiODgyMGVmNTZkMGJiOWQ1NDM1MyIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiI3NzM1NGIxMTQ4MTA4ZDIzYWQ5OTI5NDE4MmViYmJmNzM1NzIyNjg4YTdjOGFmYTQxOTM4ZGVkNzUwY2VlZTQzIiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6IjVhZjY1OTQ1YTlkNTEyODUwM2RjMTM3ZDgwM2ZmMzg5OTJhNTQ4MTg3ZjRlZjJkNThlMGQ4MjMwYjE5ZDZlZWMiLCJwYXRoIjoiaXNzdWVyLmlkZW50aXR5UHJvb2YuaWRlbnRpdHlQcm9vZlR5cGUifSx7InZhbHVlIjoiN2IxOTE4NjJiNWY0OWQxY2M1NTA0OWI0NGI2ODk4YmQwNThhMjlkNzg5MjljZDZjYjk2MWNiODgwOWE5ZjBmZSIsInBhdGgiOiJpc3N1ZXIuaWRlbnRpdHlQcm9vZi5pZGVudGlmaWVyIn0seyJ2YWx1ZSI6ImI5OGQxMTI0Y2RhYTU2NGY4YjJhZjE1OTE0OWM0MDBiODM4NWZhN2YxZThhYTRjNmExODEzNzg0NWUwMDIyMDEiLCJwYXRoIjoidmFsaWRGcm9tIn0seyJ2YWx1ZSI6ImFkYTY5ZDZlYjI3OWVjNjk0ZGE1ODMxZDU1N2Q3NjIxYWM4NWQwYzJlN2IxNDdmM2E5ZGNkMjM0NzA3MWZjMzkiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiMDdiYjI0NDRkZmQxMWVkY2M5OWI1MWJhMzllNTI4ODM3NWY2YTExY2U5YTg4Y2MxMjZkNmY4YjYxMzM2ZmQ0NiIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC50eXBlWzBdIn0seyJ2YWx1ZSI6ImZlZGIyMDRmNWM4ZWM1OGU3NzUxYTNhNWM3YmU2MmQxZjhkYjg2MTAyOTI1ZGU1MzcyY2E3NGNlZWVlZjhiOWUiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubmFtZSJ9LHsidmFsdWUiOiJjYTM4YjM3MDk0OTM5ZTE0MDNhN2RlNGUzNzljYTAzMzIzNTQ4ZTdmZjc2YmQ1MGI1ZmI0MGY4NjdmYzRkZjk2IiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzBdLmNsYXNzIn0seyJ2YWx1ZSI6IjAxY2NkZjAxMTI3YWRiODAzYjJhNmQ2YmVjZWU2YTk4N2EyNzM1ZWIxYjYzZGI0Njg5NDMyYmY2NzNmMzIwYzYiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMF0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiZDJlZGY0NmZiMjQwNzRjNzY0Y2I4MzliMjYyNGNmNjQ5NWU2NTY5NzA1YmI3ZmMyZjllZjYxNDc0ZTdkODBjYSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1swXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6ImY4OWQyOWEwOGE5NmI2YjYyNWU5NjM0OWFiMGVlM2JmNDUzODZhODAxMDhiMTE2NTUwMjk0ODg3MmE4ZmExYmYiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMV0uY2xhc3MifSx7InZhbHVlIjoiMDdmY2JmMzhjYWYxOTYyZjI5MTBjNjk1ZGY1OTVlOWYzYzUwNjk2OWRlNDVlOWU5NWNhNzAwYjFiN2E1OGZlMSIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1sxXS5kZXNjcmlwdGlvbiJ9LHsidmFsdWUiOiIyYzYyNGQ0NTJmNWE1MGJiMzlkMzY0NDMwMDA5NDYyMDY2NmY2NTk5MzJiODhmMmFjZWNjYzRiYzQwMmQ4MmNlIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzFdLmVmZmVjdGl2ZURhdGUifSx7InZhbHVlIjoiZmE5NWEyOGRhNjRlMjA4OTBiZWVjMTZiNGY2N2E2ZDMyNThkYThjYTljN2M1MDljNTZjYjMwZjg0NGUzMjNjOCIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0uaWQifSx7InZhbHVlIjoiYTZmM2FlMmYyNjM0OTkxNzg2NzZlZmMzZmVlYjc3OWEwM2IyMDlhOTQwN2Q1NWU1MDNlN2U4YTA5OWExYmFiMSIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0udHlwZSJ9LHsidmFsdWUiOiIyYjEyNGIzOGE0MDEyOWM1ZTM3OTZhZjQ3ZTEyZDg4YWI2YWZmNjllNzZiMTgyYWJmNTZiYjQ2OWU1MTZlNjc1IiwicGF0aCI6InJlbmRlck1ldGhvZFswXS50ZW1wbGF0ZU5hbWUifV0=", - privacy: { obfuscated: [] }, + "W3sidmFsdWUiOiIzMThlMDgwY2NjZWYwZmRjMWEyNjhjN2FjOTEwNmNiYzUwNTEyMzkyNTc0MDNhZTU2MmI3YTFhNmU5YjkzY2ZjIiwicGF0aCI6IkBjb250ZXh0WzBdIn0seyJ2YWx1ZSI6IjE2MTJkMDliZjUzNGZkMmZhNTFmYjlhOWI0NTk1MzE0NTlmNzU3NDA1ZmExMjllMjY3YWEwNTUxOTlhZDY2ZjAiLCJwYXRoIjoiQGNvbnRleHRbMV0ifSx7InZhbHVlIjoiY2NlMDA0ZGM0M2NiZGY0ZmE5NjRlN2Q3ZjQ0YzQ5ZTMzNDhiMzExZmQxYjc0MzhhYTI2ZjI5YmI2OGU5MGU3NCIsInBhdGgiOiJuYW1lIn0seyJ2YWx1ZSI6IjNhZmJmZDc0Mzg1YWQ2NzM0YjE3MDgyOGJhNWUyYWI1YzM4MTY1YTBmYWNlOTJlNGUwODRmNzkyMjFlZDJkMDAiLCJwYXRoIjoidHlwZVswXSJ9LHsidmFsdWUiOiIxYTMwZTE4MTI3N2I2ZjFlZTczOTlmMTQxZWU5MjY2MDIwMWUxYTRlYmZkODEzOTJiNjgxYTEwMjMxMTUzY2Q1IiwicGF0aCI6InR5cGVbMV0ifSx7InZhbHVlIjoiMWU4ZDRlODY5ZjVkMzE1YmIxMzcyZjRhOTQxYWNjNjgxMWY2OWIwZmRiMmFkOWM3MTRjMGJiNTZhMmMzMjdmZiIsInBhdGgiOiJpc3N1ZXIuaWQifSx7InZhbHVlIjoiYzVjN2MxNzVhNmU5ZTFlZDQ1OTkxYTExYjhiNjczZmRiNWU4ZTU4NDU5OTliM2FkODYwMjM5OTdiOWUzNWEzNyIsInBhdGgiOiJpc3N1ZXIudHlwZSJ9LHsidmFsdWUiOiIyMjNkZDJhOGMwZTZhOGJlMTE2ZmUxMzliM2Q0YjRkYWZlMTBkNWJiNWEwNmVjNzE2YjNhM2JiNDZlYjI3ZDNjIiwicGF0aCI6Imlzc3Vlci5uYW1lIn0seyJ2YWx1ZSI6IjM0YzJmMTU4OWM2MTFmYWZmOTE4MTAwODk3MGJiMGY1NTc3MWQ1ODI0ODQxOGY3OGVhOWM4NTk3NTgwZDM1ZTkiLCJwYXRoIjoiaXNzdWVyLmlkZW50aXR5UHJvb2YuaWRlbnRpdHlQcm9vZlR5cGUifSx7InZhbHVlIjoiYWE0MmVhNDE3MGJhNmRhMTVmZTdiMzIyZTUxMzc3ODA5M2E2NjE3MTE1NzZmOTA2M2JmY2Y4YzdlMGEzYjRhYSIsInBhdGgiOiJpc3N1ZXIuaWRlbnRpdHlQcm9vZi5pZGVudGlmaWVyIn0seyJ2YWx1ZSI6ImU5ZjNlMTJiYTUzZDA3M2Y0ZjNiZTU2ZDUzN2ZjYjIwZmI0ZTM0YzZkM2RlZGZkNTcyMzkzODAzOTViZGM4ZTkiLCJwYXRoIjoidmFsaWRGcm9tIn0seyJ2YWx1ZSI6IjhkNGIyMDYyZmFmNzRhNmZhNGE5NmE2OTQ0NzIyMWFjNjhjZTk0MjBjOWQ2ZTFhNDFjODM2MmI1ODQwMmI5ODQiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QuaWQifSx7InZhbHVlIjoiOTMxNmM0YTRhZGJiMzQwYTBjYTViNDljNTZmNWIxZWMzNjA5N2UyMjg4YzJkY2I0YmU4ZjhiY2MxNDA4NjIyYyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC50eXBlWzBdIn0seyJ2YWx1ZSI6IjJlZmY0Y2JlZjFkNmQ1ZTEwOTZhNDAyNGY4ODhmM2U2YTEwMWZkYjIxZTRjZmI1ZjdmYTA4Y2ZhZmZkMzk2YjciLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubmFtZSJ9LHsidmFsdWUiOiI0YjAxYjNmZWIzOTcyYTBmMDBjNTJkNDU2YmYxZWY1ZWJkODI3YjkxMDFjMzI4NTQwNzExM2NiODZhODNiY2JmIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzBdLmNsYXNzIn0seyJ2YWx1ZSI6IjcxMjE4ZTA2YTNlNGM1NTc5NTIwNzdkNzQ1NTJlZTMyNDIxNzMxMjU2YTcwZDI1Nzg3ZGQyNmFkMDk1YmY5NzciLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMF0uZGVzY3JpcHRpb24ifSx7InZhbHVlIjoiNWM2NzdhYzgwMGQ3MDRiZjUwYzZiYjAyN2Q0OTg1YmQ2OTg0Y2VhYjExNzUzOTk0ZWQ5YTI5OTIxYjhmYWRmNyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1swXS5lZmZlY3RpdmVEYXRlIn0seyJ2YWx1ZSI6ImVhMjY1MDM0OTFlYjhjZjNiN2EzMTg1MGEwZTM3OTQxMjFiN2YzOTQ5OTI1NmIyNGQ3OWNkODE3MDVjNDcxOTgiLCJwYXRoIjoiY3JlZGVudGlhbFN1YmplY3QubGljZW5zZXNbMV0uY2xhc3MifSx7InZhbHVlIjoiNzlhOWZiOTVkM2U5NTlkNjFlODFhNmQwMGY3ZTlkZjAwZTAzNTMzYTYyZDZlZDkyNWI4MTIyNjY5YThkYzZkNyIsInBhdGgiOiJjcmVkZW50aWFsU3ViamVjdC5saWNlbnNlc1sxXS5kZXNjcmlwdGlvbiJ9LHsidmFsdWUiOiI0YzhkOWQ0YzRkYjRhODU4NmJkODgzZmViZmNhNTUzNmYxYmVmNDJhM2NmYTJmYTQxNWY0YWFkZjIyZmY1MTlkIiwicGF0aCI6ImNyZWRlbnRpYWxTdWJqZWN0LmxpY2Vuc2VzWzFdLmVmZmVjdGl2ZURhdGUifSx7InZhbHVlIjoiNGNkOWMwOGU3NDE2MzU0YjhiNjBlYzA1YzQzZDY3YWMwOTAzNmQ4YWRlZGYyZGVjMjIxNWU1NmU4MGM1MDg5MCIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0uaWQifSx7InZhbHVlIjoiOGRjNGY3NDllN2JhZmZmZWJmM2FmOGY4ZDJjYjUwNTAzZGFmOGZhZTVkM2Q3YjhjNTNhZDM1NTFiMWM1NDI0MyIsInBhdGgiOiJyZW5kZXJNZXRob2RbMF0udHlwZSJ9LHsidmFsdWUiOiJkZjY0NmI2YjYwMTJmZWQxYzE5N2E5MjhjNGJjZTVhOWJlNTc0YjU4YmFhYjZkN2E4OTAwZDBiZDdkYjg4N2IyIiwicGF0aCI6InJlbmRlck1ldGhvZFswXS50ZW1wbGF0ZU5hbWUifV0=", + privacy: { + obfuscated: [], + }, }, }; From d6ff500b345f6083e5c9556786ca42f2ef314d68 Mon Sep 17 00:00:00 2001 From: Phan Shi Yu Date: Fri, 26 Apr 2024 18:08:56 +0800 Subject: [PATCH 43/43] refactor: improve type safety of wrap document v4 --- src/4.0/types.ts | 3 +++ src/4.0/wrap.ts | 12 +++++++++--- src/index.ts | 16 ++++------------ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/4.0/types.ts b/src/4.0/types.ts index 6f853a47..8ed7eaed 100644 --- a/src/4.0/types.ts +++ b/src/4.0/types.ts @@ -260,3 +260,6 @@ type Override, OverrideWith extends Recor /** Used to assert that StricterType is a stricter or equal version of LooserType, and most importantly, that * StricterType is STILL assignable to LooserType. */ type AssertStricterOrEqual = StricterType extends LooserType ? StricterType : never; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export type NoExtraProperties = NewObj extends Reference & infer _ExtraProps ? Reference : NewObj; diff --git a/src/4.0/wrap.ts b/src/4.0/wrap.ts index 07237b39..a310ee49 100644 --- a/src/4.0/wrap.ts +++ b/src/4.0/wrap.ts @@ -1,12 +1,15 @@ import { hashToBuffer, isStringArray } from "../shared/utils"; import { MerkleTree } from "../shared/merkle"; import { ContextType, ContextUrl } from "../shared/@types/document"; -import { V4Document, V4WrappedDocument, W3cVerifiableCredential } from "./types"; +import { NoExtraProperties, V4Document, V4WrappedDocument, W3cVerifiableCredential } from "./types"; import { digestCredential } from "../4.0/digest"; import { encodeSalt, salt } from "./salt"; import { interpretContexts } from "./validate"; -export const wrapDocument = async (document: T): Promise> => { +export const wrapDocument = async ( + // NoExtraProperties prevents the user from passing in a document with extra properties, which is more aligned to our validation strategy of strict + document: NoExtraProperties +): Promise> => { /* 1a. try OpenAttestation VC validation, since most user will be issuing oa v4*/ const oav4context = await V4Document.pick({ "@context": true }).safeParseAsync(document); // Superficial check on user intention let validatedRawDocument: W3cVerifiableCredential | undefined; @@ -94,7 +97,10 @@ export const wrapDocument = async (document: T): Promise; }; -export const wrapDocuments = async (documents: T[]): Promise[]> => { +export const wrapDocuments = async ( + // NoExtraProperties prevents the user from passing in a document with extra properties, which is more aligned to our validation strategy of strict + documents: NoExtraProperties[] +): Promise[]> => { // create individual verifiable credential const verifiableCredentials = await Promise.all(documents.map((document) => wrapDocument(document))); diff --git a/src/index.ts b/src/index.ts index 7abcce74..abcd820d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,6 @@ import { obfuscateVerifiableCredential as obfuscateVerifiableCredentialV3 } from import { OpenAttestationDocument as OpenAttestationDocumentV3 } from "./__generated__/schema.3.0"; import * as v4 from "./4.0/types"; -import { wrapDocument as wrapDocumentV4, wrapDocuments as wrapDocumentsV4 } from "./4.0/wrap"; import { signDocument as signDocumentV4 } from "./4.0/sign"; import { verify as verifyV4 } from "./4.0/verify"; import { digestCredential as digestCredentialV4 } from "./4.0/digest"; @@ -61,17 +60,10 @@ export function __unsafe__use__it__at__your__own__risks__wrapDocuments( - data: T -): Promise> { - return wrapDocumentV4(data); -} - -export function _unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocuments( - dataArray: T[] -): Promise[]> { - return wrapDocumentsV4(dataArray); -} +export { + wrapDocument as _unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocument, + wrapDocuments as _unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocuments, +} from "./4.0/wrap"; export const validateSchema = (document: WrappedDocument): boolean => { if (utils.isWrappedV2Document(document) || document?.version === SchemaId.v2)