Skip to content

Commit

Permalink
Chore: tidy v4 exports (#278)
Browse files Browse the repository at this point in the history
* refactor: shift context constants into 4.0

* refactor: bring context down to root of 4.0

* refactor: add custom error class, change algorithm to non enum

* refactor: only leave the bare essentials of v4 in root index, fix tests

* refactor: free up 4.0 from the mess of dependencies caused by importing of utils

* fix: circular dependency caused by getData

* feat: v4 utility guards

* feat: export v4
  • Loading branch information
phanshiyu committed May 6, 2024
1 parent 4cd2187 commit bf462f8
Show file tree
Hide file tree
Showing 25 changed files with 186 additions and 70 deletions.
17 changes: 17 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@
"module": "dist/esm/index.js",
"browser": "dist/index.umd.js",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts"
},
"./4.0": {
"import": "./dist/esm/4.0/exports/index.js",
"require": "./dist/cjs/4.0/exports/index.js",
"types": "./dist/types/4.0/exports/index.d.ts"
},
"./4.0/*": {
"import": "./dist/esm/4.0/exports/*.js",
"require": "./dist/cjs/4.0/exports/*.js",
"types": "./dist/types/4.0/exports/*.d.ts"
}
},
"scripts": {
"benchmark:qr-code": "ts-node --transpile-only benchmarks/qr-code",
"build": "npm run clean && npm run build:cjs && npm run build:esm && npm run build:umd && npm run build:type",
Expand Down
7 changes: 7 additions & 0 deletions src/2.0/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { unsaltData } from "./salt";
import { OpenAttestationDocument, WrappedDocument } from "./types";

type Extract<P> = P extends WrappedDocument<infer T> ? T : never;
export const getData = <T extends WrappedDocument<OpenAttestationDocument>>(document: T): Extract<T> => {
return unsaltData(document.data);
};
9 changes: 2 additions & 7 deletions src/4.0/__tests__/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import {
obfuscate,
validateSchema,
verifySignature,
_unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocument as wrapDocument,
_unsafe_use_it_at_your_own_risk_v4_alpha_wrapDocuments as wrapDocuments,
} from "../..";
import { obfuscate, validateSchema, verifySignature } from "../..";
import { cloneDeep, omit } from "lodash";
import { RAW_DOCUMENT_DID, SIGNED_WRAPPED_DOCUMENT_DID, WRAPPED_DOCUMENT_DID } from "../fixtures";
import { V4Document } from "../types";
import { wrapDocument, wrapDocuments } from "../wrap";

const DOCUMENT_ONE = {
...RAW_DOCUMENT_DID,
Expand Down
19 changes: 15 additions & 4 deletions src/4.0/__tests__/sign.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { signDocument } from "../../index";
import { SUPPORTED_SIGNING_ALGORITHM } from "../../shared/@types/sign";
import { Wallet } from "ethers";
import { WRAPPED_DOCUMENT_DID } from "../fixtures";
import { V4SignedWrappedDocument } from "../types";
import { signDocument } from "../sign";

describe("V4 sign", () => {
it("should sign a document", async () => {
Expand Down Expand Up @@ -87,8 +87,19 @@ describe("V4 sign", () => {
private: "0x812269266b34d2919f737daf22db95f02642f8cdc0ca673bf3f701599f4971f5",
}
)
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unsupported document type: Only OpenAttestation v2, v3 or v4 documents can be signed"`
);
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Document has not been properly wrapped:
{
"_errors": [],
"proof": {
"_errors": [],
"merkleRoot": {
"_errors": [
"Required"
]
}
}
}"
`);
});
});
11 changes: 10 additions & 1 deletion src/4.0/validate/context.ts → src/4.0/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { expand, Options, JsonLdDocument } from "jsonld";
import { fetch } from "cross-fetch";
import { ContextUrl } from "../../shared/@types/document";

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;

const preloadedContextList = [ContextUrl.v2_vc, ContextUrl.v4_alpha];
const contexts: Map<string, any> = new Map();
Expand Down
2 changes: 1 addition & 1 deletion src/4.0/diagnose.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Diagnose } from "src/shared/utils/@types/diagnose";
import type { Diagnose } from "../shared/utils/@types/diagnose";
import { V4WrappedDocument, V4SignedWrappedDocument, V4Document } from "./types";

export const v4Diagnose: Diagnose = ({ document, kind, debug }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/4.0/digest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { sortBy } from "lodash";
import { keccak256 } from "js-sha3";
import { V4Document, Salt } from "./types";
import { LeafValue, traverseAndFlatten } from "./traverseAndFlatten";
import { hashToBuffer } from "../shared/utils";
import { hashToBuffer } from "../shared/utils/hashing";

export const digestCredential = (document: V4Document, salts: Salt[], obfuscatedData: string[]) => {
// find all leaf nodes in the document and hash them
Expand Down
6 changes: 6 additions & 0 deletions src/4.0/exports/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from "./wrap";
export * from "./sign";
export * from "./obfuscate";
export * from "./verify";
export * from "./utils";
export * from "./types";
1 change: 1 addition & 0 deletions src/4.0/exports/obfuscate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { obfuscateVerifiableCredential as obfuscate, obfuscateErrors } from "../obfuscate";
1 change: 1 addition & 0 deletions src/4.0/exports/sign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { signDocument, signDocumentErrors } from "../sign";
5 changes: 5 additions & 0 deletions src/4.0/exports/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type {
V4Document as Document,
V4WrappedDocument as WrappedDocument,
V4SignedWrappedDocument as SignedWrappedDocument,
} from "../types";
6 changes: 6 additions & 0 deletions src/4.0/exports/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { v4Diagnose as diagnose } from "../diagnose";
export {
isV4Document as isDocument,
isV4WrappedDocument as isWrappedDocument,
isV4SignedWrappedDocument as isSignedWrappedDocument,
} from "../types";
1 change: 1 addition & 0 deletions src/4.0/exports/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { verify } from "../verify";
1 change: 1 addition & 0 deletions src/4.0/exports/wrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { wrapDocument, wrapDocuments, wrapDocumentErrors } from "../wrap";
25 changes: 21 additions & 4 deletions src/4.0/sign.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
import { sign } from "../shared/signer";
import { SigningKey, SUPPORTED_SIGNING_ALGORITHM } from "../shared/@types/sign";
import { SigningKey } from "../shared/@types/sign";
import { ethers } from "ethers";
import { V4Document, V4WrappedDocument, V4SignedWrappedDocument } from "./types";
import type { ZodError } from "zod";

export const signDocument = async <T extends V4Document>(
document: V4SignedWrappedDocument<T> | V4WrappedDocument<T>,
algorithm: SUPPORTED_SIGNING_ALGORITHM,
algorithm: "Secp256k1VerificationKey2018",
keyOrSigner: SigningKey | ethers.Signer
): Promise<V4SignedWrappedDocument<T>> => {
const parsedResults = V4WrappedDocument.pick({ proof: true }).passthrough().safeParse(document);
if (!parsedResults.success) {
throw new Error("Document has not been properly wrapped " + JSON.stringify(parsedResults.error));
throw new WrappedDocumentValidationError(parsedResults.error);
}

if (!SigningKey.guard(keyOrSigner) && keyOrSigner.signMessage === undefined) {
throw new Error(`Either a keypair or ethers.js Signer must be provided`);
}

const { proof: validatedProof } = parsedResults.data;
const merkleRoot = `0x${validatedProof.merkleRoot}`;
const signature = await sign(algorithm, merkleRoot, keyOrSigner);
const proof: V4SignedWrappedDocument["proof"] = {
...validatedProof,
key: SigningKey.guard(keyOrSigner) ? keyOrSigner.public : `did:ethr:${await keyOrSigner.getAddress()}#controller`,
key: "public" in keyOrSigner ? keyOrSigner.public : `did:ethr:${await keyOrSigner.getAddress()}#controller`,
signature,
};
return { ...document, proof };
};

class WrappedDocumentValidationError extends Error {
constructor(public error: ZodError) {
super(`Document has not been properly wrapped: \n ${JSON.stringify(error.format(), null, 2)}`);
Object.setPrototypeOf(this, WrappedDocumentValidationError.prototype);
}
}

export const signDocumentErrors = {
WrappedDocumentValidationError,
};
14 changes: 13 additions & 1 deletion src/4.0/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import z from "zod";
import { ContextUrl, ContextType } from "../shared/@types/document";
import { ContextUrl, ContextType } from "./context";

// Custom URI validation function
const URI_REGEX =
Expand Down Expand Up @@ -305,3 +305,15 @@ export type PartialDeep<T> = T extends string | number | bigint | boolean | null
: {
[K in keyof T]?: PartialDeep<T[K]>;
};

export const isV4Document = (document: unknown): document is V4Document => {
return V4Document.safeParse(document).success;
};

export const isV4WrappedDocument = (document: unknown): document is V4WrappedDocument => {
return V4WrappedDocument.safeParse(document).success;
};

export const isV4SignedWrappedDocument = (document: unknown): document is V4SignedWrappedDocument => {
return V4SignedWrappedDocument.safeParse(document).success;
};
1 change: 0 additions & 1 deletion src/4.0/validate/index.ts

This file was deleted.

9 changes: 6 additions & 3 deletions src/4.0/wrap.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { hashToBuffer, isStringArray } from "../shared/utils";
import { hashToBuffer } from "../shared/utils/hashing";
import { MerkleTree } from "../shared/merkle";
import { ContextType, ContextUrl } from "../shared/@types/document";
import { ContextUrl, ContextType, UnableToInterpretContextError, interpretContexts } from "./context";
import { NoExtraProperties, V4Document, V4WrappedDocument, W3cVerifiableCredential } from "./types";
import { digestCredential } from "../4.0/digest";
import { encodeSalt, salt } from "./salt";
import { UnableToInterpretContextError, interpretContexts } from "./validate";
import { ZodError } from "zod";

export const wrapDocument = async <T extends V4Document>(
Expand Down Expand Up @@ -151,3 +150,7 @@ export const wrapDocumentErrors = {
DataModelValidationError,
UnableToInterpretContextError,
};

function isStringArray(input: unknown): input is string[] {
return Array.isArray(input) && input.every((i) => typeof i === "string");
}
30 changes: 7 additions & 23 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,13 @@ import { digestCredential as digestCredentialV3 } from "./3.0/digest";
import { obfuscateVerifiableCredential as obfuscateVerifiableCredentialV3 } from "./3.0/obfuscate";
import { OpenAttestationDocument as OpenAttestationDocumentV3 } from "./__generated__/schema.3.0";

import * as v4 from "./4.0/types";
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 {
ObfuscateVerifiableCredentialResult,
obfuscateVerifiableCredential as obfuscateVerifiableCredentialV4,
} from "./4.0/obfuscate";
import { v4Diagnose } from "./4.0/diagnose";
import { V4WrappedDocument } from "./4.0/types";

export function wrapDocument<T extends OpenAttestationDocumentV2>(
data: T,
Expand Down Expand Up @@ -63,11 +61,6 @@ export function __unsafe__use__it__at__your__own__risks__wrapDocuments<T extends
return wrapDocumentsV3(dataArray, options ?? { version: SchemaId.v3 });
}

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<any>): boolean => {
if (utils.isWrappedV2Document(document) || document?.version === SchemaId.v2)
return validate(document, getSchema(SchemaId.v2)).length === 0;
Expand All @@ -88,18 +81,10 @@ export function verifySignature<T extends WrappedDocument<OpenAttestationDocumen
throw new Error("Unsupported document type: Only OpenAttestation v2, v3 or v4 documents can be signature verified");
}

export function digest(document: OpenAttestationDocumentV3, salts: v3.Salt[], obfuscatedData: string[]): string;
export function digest(document: v4.V4Document, salts: v4.Salt[], obfuscatedData: string[]): string;
export function digest(
document: OpenAttestationDocumentV3 | v4.V4Document,
salts: v3.Salt[] | v4.Salt[],
obfuscatedData: string[]
): string {
export function digest(document: OpenAttestationDocumentV3, salts: v3.Salt[], obfuscatedData: string[]): string {
if (utils.isRawV3Document(document)) return digestCredentialV3(document, salts, obfuscatedData);
else if (utils.isRawV4Document(document)) return digestCredentialV4(document, salts, obfuscatedData);

throw new Error(
"Unsupported credential type: This function only supports digest generation for OpenAttestation v3 or v4 credentials"
"Unsupported credential type: This function only supports digest generation for OpenAttestation v3 credentials"
);
}

Expand All @@ -111,7 +96,7 @@ export function obfuscate<T extends OpenAttestationDocumentV3>(
document: WrappedDocument<T>,
fields: string[] | string
): WrappedDocument<T>;
export function obfuscate<T extends v4.V4WrappedDocument>(
export function obfuscate<T extends V4WrappedDocument>(
document: T,
fields: string[] | string
): ObfuscateVerifiableCredentialResult<T>;
Expand All @@ -127,7 +112,7 @@ export const isSchemaValidationError = (error: any): error is SchemaValidationEr
return !!error.validationErrors;
};

export async function signDocument<T extends v2.OpenAttestationDocument | v3.OpenAttestationDocument | v4.V4Document>(
export async function signDocument<T extends v2.OpenAttestationDocument | v3.OpenAttestationDocument>(
document: WrappedDocument<T> | SignedWrappedDocument<T>,
algorithm: SUPPORTED_SIGNING_ALGORITHM,
keyOrSigner: SigningKey | ethers.Signer
Expand All @@ -140,12 +125,11 @@ export async function signDocument<T extends v2.OpenAttestationDocument | v3.Ope
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<T>;

// 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");
throw new Error("Unsupported document type: Only OpenAttestation v2 or v3documents can be signed");
}

export { digestDocument } from "./2.0/digest";
Expand All @@ -159,4 +143,4 @@ export * from "./shared/signer";
export { getData } from "./shared/utils"; // keep it to avoid breaking change, moved from privacy to utils
export { v2 };
export { v3 };
export { v4 };
export * as v4 from "./4.0/exports";
10 changes: 0 additions & 10 deletions src/shared/@types/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,6 @@ export enum SchemaId {
v3 = "https://schema.openattestation.com/3.0/schema.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`
);
Expand Down
2 changes: 1 addition & 1 deletion src/shared/merkle/merkle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Hash, hashArray, toBuffer, hashToBuffer, combineHashBuffers } from "../utils";
import { Hash, hashArray, toBuffer, hashToBuffer, combineHashBuffers } from "../utils/hashing";

function getNextLayer(elements: Buffer[]) {
return elements.reduce((layer: Buffer[], element, index, arr) => {
Expand Down

0 comments on commit bf462f8

Please sign in to comment.