@idos-network/credentials 1.2.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +289 -85
- package/assets/idos-credential-subject-edd-v1.json +10 -0
- package/assets/idos-credential-subject-v1.json +1 -1
- package/assets/idos-credential-subject-v2.json +1 -1
- package/assets/idos-credential-subject-v3.json +83 -0
- package/assets/idos-credentials-v1.json +2 -2
- package/assets/idos-credentials-v2.json +10 -0
- package/dist/index-0NqXMjNk.d.mts +395 -0
- package/dist/index-0NqXMjNk.d.mts.map +1 -0
- package/dist/index-WW372Tf5.d.mts +57 -0
- package/dist/index-WW372Tf5.d.mts.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/loader-Cg6hs1-P.mjs +2 -0
- package/dist/loader-Cg6hs1-P.mjs.map +1 -0
- package/dist/parser/index.d.mts +19 -0
- package/dist/parser/index.d.mts.map +1 -0
- package/dist/parser/index.mjs +2 -0
- package/dist/parser/index.mjs.map +1 -0
- package/dist/schemas/index.d.mts +2 -0
- package/dist/schemas/index.mjs +1 -0
- package/dist/schemas-BVGuUlPe.mjs +2 -0
- package/dist/schemas-BVGuUlPe.mjs.map +1 -0
- package/dist/types/index.d.mts +2 -135
- package/dist/types/index.mjs +1 -63
- package/dist/utils/index.d.mts +38 -22
- package/dist/utils/index.d.mts.map +1 -1
- package/dist/utils/index.mjs +1 -123
- package/dist/utils/index.mjs.map +1 -1
- package/dist/verifier/index.d.mts +8 -0
- package/dist/verifier/index.d.mts.map +1 -0
- package/dist/verifier/index.mjs +2 -0
- package/dist/verifier/index.mjs.map +1 -0
- package/package.json +31 -15
- package/src/parser/index.test.ts +266 -0
- package/src/parser/index.ts +69 -0
- package/src/schemas/EddV1/index.test.ts +40 -0
- package/src/schemas/EddV1/index.ts +53 -0
- package/src/schemas/Envelope.ts +37 -0
- package/src/schemas/FaceIdV1/index.test.ts +26 -0
- package/src/schemas/FaceIdV1/index.ts +33 -0
- package/src/schemas/KycV1/IdDocument.ts +41 -0
- package/src/schemas/KycV1/ResidentialAddress.ts +56 -0
- package/src/schemas/KycV1/Subject.ts +86 -0
- package/src/schemas/KycV1/index.test.ts +156 -0
- package/src/schemas/KycV1/index.ts +40 -0
- package/src/schemas/KycV2/Subject.ts +67 -0
- package/src/schemas/KycV2/index.test.ts +162 -0
- package/src/schemas/KycV2/index.ts +45 -0
- package/src/schemas/KycV3/Biometric.ts +14 -0
- package/src/schemas/KycV3/Contact.ts +13 -0
- package/src/schemas/KycV3/EDD.ts +32 -0
- package/src/schemas/KycV3/IdDocument.ts +92 -0
- package/src/schemas/KycV3/Onboarding.ts +36 -0
- package/src/schemas/KycV3/Person.ts +105 -0
- package/src/schemas/KycV3/ResidentialAddress.ts +73 -0
- package/src/schemas/KycV3/Screening.ts +27 -0
- package/src/schemas/KycV3/SourceOfWealth.ts +44 -0
- package/src/schemas/KycV3/Subject.ts +54 -0
- package/src/schemas/KycV3/index.test.ts +682 -0
- package/src/schemas/KycV3/index.ts +108 -0
- package/src/schemas/VerifiableCredential.ts +232 -0
- package/src/schemas/enums.ts +126 -0
- package/src/schemas/index.ts +14 -0
- package/src/schemas/integration.test.ts +251 -0
- package/src/schemas/types.ts +11 -0
- package/src/schemas/utils.ts +274 -0
- package/src/types/credentials.ts +18 -249
- package/src/utils/{utils.test.ts → index.test.ts} +59 -67
- package/src/utils/index.ts +129 -137
- package/src/utils/issuer.ts +42 -0
- package/src/{builder → utils}/loader.ts +22 -17
- package/src/verifier/index.test.ts +149 -0
- package/src/{builder/verifier.ts → verifier/index.ts} +4 -8
- package/types/internals.d.ts +11 -6
- package/dist/builder/index.d.mts +0 -20
- package/dist/builder/index.d.mts.map +0 -1
- package/dist/builder/index.mjs +0 -619
- package/dist/builder/index.mjs.map +0 -1
- package/dist/types/index.d.mts.map +0 -1
- package/dist/types/index.mjs.map +0 -1
- package/src/builder/builder.test.ts +0 -407
- package/src/builder/index.ts +0 -122
package/dist/utils/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/utils/index.ts"],"sourcesContent":["import { Ed25519VerificationKey2020 } from \"@digitalbazaar/ed25519-verification-key-2020\";\nimport { base64Decode, base64Encode, hexEncode, utf8Encode } from \"@idos-network/utils/codecs\";\nimport * as base85 from \"base85\";\nimport { every, get } from \"es-toolkit/compat\";\nimport invariant from \"tiny-invariant\";\nimport nacl from \"tweetnacl\";\n\nimport type {\n AvailableIssuerType,\n CredentialFields,\n CredentialResidentialAddress,\n CredentialSubject,\n CredentialSubjectFaceId,\n CustomIssuerType,\n InsertableIDOSCredential,\n} from \"../types\";\n\nexport function fileToBase85(file: Buffer): string {\n return base85.encode(file, \"ascii85\");\n}\n\nexport function base85ToFile(data: string): Buffer | false {\n return base85.decode(data);\n}\n\nexport function capitalizeFirstLetter(str: string): string {\n return str[0].toUpperCase() + str.slice(1);\n}\n\nfunction isIssuerKey(issuer: AvailableIssuerType): issuer is Ed25519VerificationKey2020 {\n return (\n typeof issuer === \"object\" &&\n issuer !== null &&\n \"type\" in issuer &&\n \"id\" in issuer &&\n \"controller\" in issuer\n );\n}\n\nfunction isCustomIssuerType(issuer: AvailableIssuerType): issuer is CustomIssuerType {\n return (\n typeof issuer === \"object\" &&\n issuer !== null &&\n \"issuer\" in issuer &&\n \"publicKeyMultibase\" in issuer\n );\n}\n\nexport async function issuerToKey(\n issuer: AvailableIssuerType,\n): Promise<Ed25519VerificationKey2020> {\n if (isIssuerKey(issuer)) {\n return issuer;\n }\n\n if (isCustomIssuerType(issuer)) {\n return await Ed25519VerificationKey2020.from({\n id: `${issuer.issuer}/keys/1`,\n controller: `${issuer.issuer}/issuers/1`,\n publicKeyMultibase: issuer.publicKeyMultibase,\n privateKeyMultibase: issuer.privateKeyMultibase,\n type: \"Ed25519VerificationKey2020\",\n });\n }\n\n return await Ed25519VerificationKey2020.from({ ...issuer, type: \"Ed25519VerificationKey2020\" });\n}\n\nexport function convertValues<\n K extends\n | CredentialFields\n | CredentialSubject\n | CredentialResidentialAddress\n | CredentialSubjectFaceId,\n>(fields: K, prefix?: string): Record<string, unknown> {\n const acc: Record<string, unknown> = {};\n\n for (const key in fields) {\n if (Object.hasOwn(fields, key)) {\n const value = fields[key];\n const name = prefix ? `${prefix}${capitalizeFirstLetter(key)}` : key;\n if (value instanceof Date) {\n acc[name] = value.toISOString();\n } else if (value instanceof Buffer) {\n // Convert file to base85\n acc[name] = fileToBase85(value);\n } else {\n acc[name] = value;\n }\n }\n }\n\n return acc;\n}\n\ntype BaseLevel = \"basic\" | \"plus\";\ntype Addon = \"liveness\" | \"email\" | \"phoneNumber\";\n\nexport function deriveLevel(credential: CredentialSubject): string {\n let level: BaseLevel = \"basic\";\n\n // Address is a sign for plus+\n const address = credential.residentialAddress;\n if (address?.proofFile && address?.city && address?.proofCategory) {\n level = \"plus\";\n }\n\n const addons: Addon[] = [];\n if (credential.selfieFile) {\n addons.push(\"liveness\");\n }\n\n if (credential.email) {\n addons.push(\"email\");\n }\n\n if (credential.phoneNumber) {\n addons.push(\"phoneNumber\");\n }\n\n return [level, ...addons].join(\"+\");\n}\n\nexport function parseLevel(level: string): {\n base: BaseLevel;\n addons: Addon[];\n} {\n const [base, ...addons] = level.split(\"+\") as [BaseLevel, ...Addon[]];\n return { base, addons };\n}\n\nexport function matchLevelOrHigher(\n level: BaseLevel,\n requiredAddons: Addon[],\n currentLevel: string,\n): boolean {\n const { base: currentBaseLevel, addons: currentAddons } = parseLevel(currentLevel);\n\n // TODO: Consider pop+ or uniqueness+ scenarios\n if (level === \"plus\" && currentBaseLevel !== \"plus\") {\n return false;\n }\n\n return requiredAddons.every((addon) => currentAddons.includes(addon));\n}\n\nexport function levelScore(level: string): number {\n const { base, addons } = parseLevel(level);\n let score = 0;\n\n if (base === \"plus\") {\n score += 100;\n }\n\n score += addons.length * 10;\n\n return score;\n}\n\nexport function pickHighestMatchingLevel(\n levels: string[],\n requiredLevel: BaseLevel,\n requiredAddons: Addon[],\n): string | null {\n return (\n levels\n .filter((currentLevel) => matchLevelOrHigher(requiredLevel, requiredAddons, currentLevel))\n .sort((a, b) => {\n const aAddons = levelScore(a);\n const bAddons = levelScore(b);\n return bAddons - aAddons; // descending\n })[0] ?? null\n );\n}\n\nexport function highestMatchingCredential<K extends { public_notes: string }>(\n credentials: K[],\n requiredLevel: BaseLevel,\n {\n addons: requiredAddons = [],\n publicNotesConstraint = {},\n }: {\n addons?: Addon[];\n publicNotesConstraint?: Record<string, number | string>;\n },\n): K | undefined {\n const matchingCredentials = credentials\n .map((credential) => {\n const publicNotes = JSON.parse(credential.public_notes || \"{}\");\n return {\n credential,\n publicNotes,\n };\n })\n .filter(({ publicNotes }) => {\n const level = publicNotes.level;\n\n if (!level) {\n return false;\n }\n\n if (!matchLevelOrHigher(requiredLevel, requiredAddons, level)) {\n return false;\n }\n\n for (const key in publicNotesConstraint) {\n if (publicNotes[key] !== publicNotesConstraint[key]) {\n return false;\n }\n }\n\n return true;\n })\n .sort((a, b) => {\n const aLevel = a.publicNotes.level;\n const bLevel = b.publicNotes.level;\n return levelScore(bLevel) - levelScore(aLevel); // descending\n })\n .map(({ credential }) => credential);\n\n return matchingCredentials[0];\n}\n\nexport function recordFilter(\n rec: Record<string, unknown>,\n pick: Record<string, unknown[]>,\n omit: Record<string, unknown[]>,\n): boolean {\n const matchCriteria = (content: Record<string, unknown>, criteria: Record<string, unknown[]>) =>\n every(Object.entries(criteria), ([path, targetSet]) => targetSet.includes(get(content, path)));\n\n if (Object.keys(pick).length > 0 && !matchCriteria(rec, pick)) {\n // Fast fail on pick criteria\n return false;\n }\n\n if (Object.keys(omit).length > 0 && matchCriteria(rec, omit)) {\n // Fast fail on omit criteria\n return false;\n }\n\n return true;\n}\n\nexport function buildInsertableIDOSCredential(\n userId: string,\n publicNotes: string,\n content: string,\n encryptorPublicKey: string,\n): InsertableIDOSCredential {\n invariant(encryptorPublicKey, \"Missing `encryptorPublicKey`\");\n\n const ephemeralAuthenticationKeyPair = nacl.sign.keyPair();\n\n const publicNotesSignature = nacl.sign.detached(\n utf8Encode(publicNotes),\n ephemeralAuthenticationKeyPair.secretKey,\n );\n\n return {\n user_id: userId,\n content,\n\n public_notes: publicNotes,\n public_notes_signature: base64Encode(publicNotesSignature),\n\n broader_signature: base64Encode(\n nacl.sign.detached(\n Uint8Array.from([...publicNotesSignature, ...base64Decode(content)]),\n ephemeralAuthenticationKeyPair.secretKey,\n ),\n ),\n\n issuer_auth_public_key: hexEncode(ephemeralAuthenticationKeyPair.publicKey, true),\n encryptor_public_key: encryptorPublicKey,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/utils/issuer.ts","../../src/utils/index.ts"],"sourcesContent":["import { Ed25519VerificationKey2020 } from \"@digitalbazaar/ed25519-verification-key-2020\";\n\nimport type { AvailableIssuerType, CustomIssuerType } from \"../types\";\n\nfunction isIssuerKey(issuer: AvailableIssuerType): issuer is Ed25519VerificationKey2020 {\n return (\n typeof issuer === \"object\" &&\n issuer !== null &&\n \"type\" in issuer &&\n \"id\" in issuer &&\n \"controller\" in issuer\n );\n}\n\nfunction isCustomIssuerType(issuer: AvailableIssuerType): issuer is CustomIssuerType {\n return (\n typeof issuer === \"object\" &&\n issuer !== null &&\n \"issuer\" in issuer &&\n \"publicKeyMultibase\" in issuer\n );\n}\n\nexport async function issuerToKey(\n issuer: AvailableIssuerType,\n): Promise<Ed25519VerificationKey2020> {\n if (isIssuerKey(issuer)) {\n return issuer;\n }\n\n if (isCustomIssuerType(issuer)) {\n return await Ed25519VerificationKey2020.from({\n id: `${issuer.issuer}/keys/1`,\n controller: `${issuer.issuer}/issuers/1`,\n publicKeyMultibase: issuer.publicKeyMultibase,\n privateKeyMultibase: issuer.privateKeyMultibase,\n type: \"Ed25519VerificationKey2020\",\n });\n }\n\n return await Ed25519VerificationKey2020.from({ ...issuer, type: \"Ed25519VerificationKey2020\" });\n}\n","import { createBlobContentReference } from \"@idos-network/utils/blob-gateway\";\nimport { base64Encode, hexEncode, utf8Encode } from \"@idos-network/utils/codecs\";\nimport { encryptContent } from \"@idos-network/utils/cryptography\";\nimport { every, get } from \"es-toolkit/compat\";\nimport nacl from \"tweetnacl\";\n\nimport type { idOSCredential, SignedCredentialContentReference } from \"../types\";\n\n// Proxying functions\nexport * from \"./issuer\";\n\nexport type BaseLevel = \"unverified\" | \"basic\" | \"plus\";\nexport type Addon =\n | \"liveness\"\n | \"email\"\n | \"phoneNumber\"\n | \"edd\"\n | \"sow\"\n | \"screening\"\n | \"onboarding\";\n\nexport function parseLevel(level: string): {\n base: BaseLevel;\n addons: Addon[];\n} {\n const [base, ...addons] = level.split(\"+\") as [BaseLevel, ...Addon[]];\n return { base, addons };\n}\n\nexport function assertNoExtraFields(\n section: string,\n expectedFields: ReadonlySet<string>,\n value: object | undefined,\n): void {\n const extraFields = Object.keys(value ?? {}).filter((field) => !expectedFields.has(field));\n\n if (extraFields.length > 0) {\n throw new Error(`Unexpected ${section} fields: ${extraFields.join(\", \")}`);\n }\n}\n\nexport function matchLevelOrHigher(\n level: BaseLevel,\n requiredAddons: Addon[],\n currentLevel: string,\n): boolean {\n const { base: currentBaseLevel, addons: currentAddons } = parseLevel(currentLevel);\n\n // Basic can only match basic or plus not unverified\n if (level === \"basic\" && currentBaseLevel === \"unverified\") {\n return false;\n }\n\n // Plus can only match plus\n if (level === \"plus\" && currentBaseLevel !== \"plus\") {\n return false;\n }\n\n return requiredAddons.every((addon) => currentAddons.includes(addon));\n}\n\nexport function levelScore(level: string): number {\n const { base, addons } = parseLevel(level);\n let score = 0;\n\n if (base === \"basic\") {\n score += 50;\n }\n\n if (base === \"plus\") {\n score += 100;\n }\n\n score += addons.length * 10;\n\n return score;\n}\n\nexport function pickHighestMatchingLevel(\n levels: string[],\n requiredLevel: BaseLevel,\n requiredAddons: Addon[],\n): string | null {\n return (\n levels\n .filter((currentLevel) => matchLevelOrHigher(requiredLevel, requiredAddons, currentLevel))\n .sort((a, b) => {\n const aAddons = levelScore(a);\n const bAddons = levelScore(b);\n return bAddons - aAddons; // descending\n })[0] ?? null\n );\n}\n\nexport function highestMatchingCredential<K extends { public_notes: string }>(\n credentials: K[],\n requiredLevel: BaseLevel,\n {\n addons: requiredAddons = [],\n publicNotesConstraint = {},\n }: {\n addons?: Addon[];\n publicNotesConstraint?: Record<string, number | string>;\n },\n): K | undefined {\n const matchingCredentials = credentials\n .map((credential) => {\n const publicNotes = JSON.parse(credential.public_notes || \"{}\");\n return {\n credential,\n publicNotes,\n };\n })\n .filter(({ publicNotes }) => {\n const level = publicNotes.level;\n\n if (!level) {\n return false;\n }\n\n if (!matchLevelOrHigher(requiredLevel, requiredAddons, level)) {\n return false;\n }\n\n for (const key in publicNotesConstraint) {\n if (publicNotes[key] !== publicNotesConstraint[key]) {\n return false;\n }\n }\n\n return true;\n })\n .sort((a, b) => {\n const aLevel = a.publicNotes.level;\n const bLevel = b.publicNotes.level;\n return levelScore(bLevel) - levelScore(aLevel); // descending\n })\n .map(({ credential }) => credential);\n\n return matchingCredentials[0];\n}\n\nexport function recordFilter(\n rec: Record<string, unknown>,\n pick: Record<string, unknown[]>,\n omit: Record<string, unknown[]>,\n): boolean {\n const matchCriteria = (content: Record<string, unknown>, criteria: Record<string, unknown[]>) =>\n every(Object.entries(criteria), ([path, targetSet]) => targetSet.includes(get(content, path)));\n\n if (Object.keys(pick).length > 0 && !matchCriteria(rec, pick)) {\n // Fast fail on pick criteria\n return false;\n }\n\n if (Object.keys(omit).length > 0 && matchCriteria(rec, omit)) {\n // Fast fail on omit criteria\n return false;\n }\n\n return true;\n}\n\nexport function buildSignedCredentialContentReference(\n publicNotes: string,\n contentUri: string,\n issuerSigningSecretKey: Uint8Array,\n): SignedCredentialContentReference {\n const { publicKey, secretKey } = nacl.sign.keyPair.fromSecretKey(issuerSigningSecretKey);\n\n const publicNotesSignature = nacl.sign.detached(utf8Encode(publicNotes), secretKey);\n\n return {\n public_notes: publicNotes,\n public_notes_signature: base64Encode(publicNotesSignature),\n\n broader_signature: base64Encode(\n nacl.sign.detached(\n Uint8Array.from([...publicNotesSignature, ...utf8Encode(contentUri)]),\n secretKey,\n ),\n ),\n\n issuer_auth_public_key: hexEncode(publicKey, true),\n };\n}\n\n/** User-issued share copy: sign with a fresh ephemeral key (need not match original issuer). */\nexport function buildEphemeralSignedCredentialContentReference(\n publicNotes: string,\n contentUri: string,\n): SignedCredentialContentReference {\n return buildSignedCredentialContentReference(\n publicNotes,\n contentUri,\n nacl.sign.keyPair().secretKey,\n );\n}\n\nexport type PreliminaryIDOSCredential = {\n id: string;\n contentUri: string;\n contentSize: number;\n encryptedContent: Uint8Array;\n publicNotes: string;\n publicNotesSignature: string;\n broaderSignature: string;\n issuerAuthPublicKey: string;\n encryptorPublicKey: string;\n};\n\nexport function mapPreliminaryToIDOSCredential(\n preliminaryCredential: PreliminaryIDOSCredential,\n): Omit<idOSCredential, \"user_id\"> {\n return {\n id: preliminaryCredential.id,\n content_uri: preliminaryCredential.contentUri,\n content_size: preliminaryCredential.contentSize,\n public_notes: preliminaryCredential.publicNotes,\n issuer_auth_public_key: preliminaryCredential.issuerAuthPublicKey,\n encryptor_public_key: preliminaryCredential.encryptorPublicKey,\n };\n}\n\nexport interface BuildPreliminaryIDOSCredentialArgs {\n publicNotes: string;\n plaintextContent: Uint8Array;\n recipientEncryptionPublicKey: Uint8Array;\n issuerSigningSecretKey?: Uint8Array;\n /** When set (MM / `ukyc://`), skip IPFS CID hashing. */\n contentUri?: string;\n}\n\nexport async function buildPreliminaryIDOSCredential({\n publicNotes,\n plaintextContent,\n recipientEncryptionPublicKey,\n // For user-issued credentials, use a fresh ephemeral key for the signed reference\n // For issuer-side, use the signing key pair\n issuerSigningSecretKey = nacl.sign.keyPair().secretKey,\n contentUri,\n}: BuildPreliminaryIDOSCredentialArgs): Promise<Omit<PreliminaryIDOSCredential, \"id\">> {\n const ephemeralKeyPair = nacl.box.keyPair();\n const encryptedContent = encryptContent(\n plaintextContent,\n recipientEncryptionPublicKey, // user or dwg recipient\n ephemeralKeyPair.secretKey,\n );\n\n const contentReference = contentUri\n ? { uri: contentUri, size: encryptedContent.byteLength }\n : await createBlobContentReference(encryptedContent);\n const signedReference = buildSignedCredentialContentReference(\n publicNotes,\n contentReference.uri,\n issuerSigningSecretKey,\n );\n\n return {\n contentUri: contentReference.uri,\n contentSize: contentReference.size,\n encryptedContent,\n publicNotes,\n publicNotesSignature: signedReference.public_notes_signature,\n broaderSignature: signedReference.broader_signature,\n issuerAuthPublicKey: signedReference.issuer_auth_public_key,\n encryptorPublicKey: base64Encode(ephemeralKeyPair.publicKey),\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { i as VerifiableCredential, t as AvailableIssuerType } from "../index-WW372Tf5.mjs";
|
|
2
|
+
import * as vc from "@digitalbazaar/vc";
|
|
3
|
+
import { JsonLDDocumentLoaderInstance } from "jsonld-document-loader";
|
|
4
|
+
//#region src/verifier/index.d.ts
|
|
5
|
+
export type VerifyCredentialResult = [boolean, Map<AvailableIssuerType, vc.VerifyCredentialResult>];
|
|
6
|
+
export declare function verifyCredential<K>(credential: VerifiableCredential<K>, issuers: AvailableIssuerType[], customDocumentLoader?: JsonLDDocumentLoaderInstance): Promise<VerifyCredentialResult>;
|
|
7
|
+
//#endregion
|
|
8
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/verifier/index.ts"],"mappings":";;;;YAUY,mCAAmC,IAAI,qBAAqB,GAAG;wBAErD,iBAAiB,GACrC,YAAY,qBAAqB,IACjC,SAAS,uBACT,uBAAuB,+BACtB,QAAQ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{issuerToKey as e}from"../utils/index.mjs";import{u as t}from"../loader-Cg6hs1-P.mjs";import{Ed25519Signature2020 as n}from"@digitalbazaar/ed25519-signature-2020";import*as r from"@digitalbazaar/vc";async function i(i,a,o){let s=new Map;for(let c of a){let a=await e(c),l=new n({key:a,verificationMethod:a.id}),u={"@context":`https://w3id.org/security/v2`,id:a.controller,assertionMethod:[a.id],authentication:[a.id]},d=await r.verifyCredential({credential:i,suite:l,controller:u,documentLoader:o??t});if(s.set(c,d),d.verified)return[!0,s]}return[!1,s]}export{i as verifyCredential};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/verifier/index.ts"],"sourcesContent":["import type { JsonLDDocumentLoaderInstance } from \"jsonld-document-loader\";\n\nimport { Ed25519Signature2020 } from \"@digitalbazaar/ed25519-signature-2020\";\nimport * as vc from \"@digitalbazaar/vc\";\n\nimport type { AvailableIssuerType, VerifiableCredential } from \"../types\";\n\nimport { issuerToKey } from \"../utils\";\nimport { defaultDocumentLoader } from \"../utils/loader\";\n\nexport type VerifyCredentialResult = [boolean, Map<AvailableIssuerType, vc.VerifyCredentialResult>];\n\nexport async function verifyCredential<K>(\n credential: VerifiableCredential<K>,\n issuers: AvailableIssuerType[],\n customDocumentLoader?: JsonLDDocumentLoaderInstance,\n): Promise<VerifyCredentialResult> {\n const resultsByIssuer: Map<AvailableIssuerType, vc.VerifyCredentialResult> = new Map();\n\n for (const issuer of issuers) {\n const publicKey = await issuerToKey(issuer);\n\n const vcVerifyingSuite = new Ed25519Signature2020({\n key: publicKey,\n verificationMethod: publicKey.id,\n });\n\n const controller = {\n \"@context\": \"https://w3id.org/security/v2\",\n id: publicKey.controller,\n assertionMethod: [publicKey.id],\n authentication: [publicKey.id],\n };\n\n // Verify the signature\n const verifyCredentialResult = await vc.verifyCredential<K>({\n credential,\n suite: vcVerifyingSuite,\n controller,\n documentLoader: customDocumentLoader ?? defaultDocumentLoader,\n });\n\n resultsByIssuer.set(issuer, verifyCredentialResult);\n\n if (verifyCredentialResult.verified) {\n return [true, resultsByIssuer];\n }\n }\n\n return [false, resultsByIssuer];\n}\n"],"mappings":"6MAYA,eAAsB,EACpB,EACA,EACA,EACiC,CACjC,IAAM,EAAuE,IAAI,IAEjF,IAAK,IAAM,KAAU,EAAS,CAC5B,IAAM,EAAY,MAAM,EAAY,CAAM,EAEpC,EAAmB,IAAI,EAAqB,CAChD,IAAK,EACL,mBAAoB,EAAU,EAChC,CAAC,EAEK,EAAa,CACjB,WAAY,+BACZ,GAAI,EAAU,WACd,gBAAiB,CAAC,EAAU,EAAE,EAC9B,eAAgB,CAAC,EAAU,EAAE,CAC/B,EAGM,EAAyB,MAAM,EAAG,iBAAoB,CAC1D,aACA,MAAO,EACP,aACA,eAAgB,GAAwB,CAC1C,CAAC,EAID,GAFA,EAAgB,IAAI,EAAQ,CAAsB,EAE9C,EAAuB,SACzB,MAAO,CAAC,GAAM,CAAe,CAEjC,CAEA,MAAO,CAAC,GAAO,CAAe,CAChC"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idos-network/credentials",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "idOS Credentials JavaScript SDK",
|
|
5
5
|
"homepage": "https://idos.network",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"repository":
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "github:idos-network/idos-sdk-js"
|
|
10
|
+
},
|
|
8
11
|
"files": [
|
|
9
12
|
"assets/**/*",
|
|
10
13
|
"dist/**/*",
|
|
@@ -25,13 +28,21 @@
|
|
|
25
28
|
"types": "./dist/utils/index.d.mts",
|
|
26
29
|
"import": "./dist/utils/index.mjs"
|
|
27
30
|
},
|
|
28
|
-
"./
|
|
29
|
-
"types": "./dist/
|
|
30
|
-
"import": "./dist/
|
|
31
|
+
"./verifier": {
|
|
32
|
+
"types": "./dist/verifier/index.d.mts",
|
|
33
|
+
"import": "./dist/verifier/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./parser": {
|
|
36
|
+
"types": "./dist/parser/index.d.mts",
|
|
37
|
+
"import": "./dist/parser/index.mjs"
|
|
31
38
|
},
|
|
32
39
|
"./types": {
|
|
33
40
|
"types": "./dist/types/index.d.mts",
|
|
34
41
|
"import": "./dist/types/index.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./schemas": {
|
|
44
|
+
"types": "./dist/schemas/index.d.mts",
|
|
45
|
+
"import": "./dist/schemas/index.mjs"
|
|
35
46
|
}
|
|
36
47
|
},
|
|
37
48
|
"publishConfig": {
|
|
@@ -40,24 +51,27 @@
|
|
|
40
51
|
"dependencies": {
|
|
41
52
|
"@digitalbazaar/ed25519-signature-2020": "^5.4.0",
|
|
42
53
|
"@digitalbazaar/ed25519-verification-key-2020": "^4.2.0",
|
|
43
|
-
"@digitalbazaar/vc": "^7.
|
|
44
|
-
"
|
|
45
|
-
"
|
|
54
|
+
"@digitalbazaar/vc": "^7.3.0",
|
|
55
|
+
"@idos-network/utils": "2.2.0",
|
|
56
|
+
"class-transformer": "^0.5.1",
|
|
57
|
+
"class-validator": "^0.15.1",
|
|
58
|
+
"es-toolkit": "^1.50.0",
|
|
46
59
|
"jsonld-document-loader": "^2.3.0",
|
|
60
|
+
"reflect-metadata": "^0.2.2",
|
|
47
61
|
"tiny-invariant": "^1.3.3",
|
|
48
62
|
"tweetnacl": "^1.0.3",
|
|
49
|
-
"zod": "4.4
|
|
50
|
-
"@idos-network/utils": "1.2.0"
|
|
63
|
+
"zod": "4.5.4"
|
|
51
64
|
},
|
|
52
65
|
"devDependencies": {
|
|
53
|
-
"@types/node": "25.
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
66
|
+
"@types/node": "25.9.5",
|
|
67
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
68
|
+
"tsdown": "^0.23.0",
|
|
69
|
+
"typescript": "^5.9.3",
|
|
70
|
+
"vitest": "4.1.11",
|
|
57
71
|
"web-streams-polyfill": "3.3.3"
|
|
58
72
|
},
|
|
59
73
|
"peerDependencies": {
|
|
60
|
-
"typescript": "^5.
|
|
74
|
+
"typescript": "^5.9.3"
|
|
61
75
|
},
|
|
62
76
|
"engines": {
|
|
63
77
|
"node": ">=22.19.0"
|
|
@@ -65,9 +79,11 @@
|
|
|
65
79
|
"scripts": {
|
|
66
80
|
"build": "tsdown",
|
|
67
81
|
"dev": "tsdown --watch",
|
|
82
|
+
"generate:schemas": "pnpm build && node ./cli/generate-schemas.mjs",
|
|
68
83
|
"publint": "pnpx publint --strict",
|
|
69
84
|
"test": "vitest",
|
|
70
85
|
"test:watch": "vitest --watch",
|
|
86
|
+
"coverage": "vitest run --coverage",
|
|
71
87
|
"typecheck": "tsc --noEmit"
|
|
72
88
|
}
|
|
73
89
|
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { VerifiableCredential } from "../types";
|
|
4
|
+
|
|
5
|
+
import { parseCredential } from ".";
|
|
6
|
+
import {
|
|
7
|
+
VerifiableCredentialEddV1,
|
|
8
|
+
VerifiableCredentialFaceIdV1,
|
|
9
|
+
VerifiableCredentialKycV1,
|
|
10
|
+
VerifiableCredentialKycV2,
|
|
11
|
+
VerifiableCredentialKycV3,
|
|
12
|
+
} from "../schemas";
|
|
13
|
+
import { CredentialValidationError } from "../schemas/utils";
|
|
14
|
+
|
|
15
|
+
const ISSUER = "https://vc-issuers.cool.id/idos";
|
|
16
|
+
const CRED_ID = `${ISSUER}/credentials/z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3`;
|
|
17
|
+
const CREDENTIALS_V1_CONTEXT =
|
|
18
|
+
"https://idos-network.github.io/idos-sdk-js/credentials/idos-credentials-v1.json";
|
|
19
|
+
const CREDENTIALS_V2_CONTEXT =
|
|
20
|
+
"https://idos-network.github.io/idos-sdk-js/credentials/idos-credentials-v2.json";
|
|
21
|
+
const FACE_ID_V1_SUBJECT_CONTEXT =
|
|
22
|
+
"https://idos-network.github.io/idos-sdk-js/credentials/idos-credential-subject-face-id-v1.json";
|
|
23
|
+
const KYC_V1_SUBJECT_CONTEXT =
|
|
24
|
+
"https://idos-network.github.io/idos-sdk-js/credentials/idos-credential-subject-v1.json";
|
|
25
|
+
const KYC_V2_SUBJECT_CONTEXT =
|
|
26
|
+
"https://idos-network.github.io/idos-sdk-js/credentials/idos-credential-subject-v2.json";
|
|
27
|
+
const KYC_V3_SUBJECT_CONTEXT =
|
|
28
|
+
"https://idos-network.github.io/idos-sdk-js/credentials/idos-credential-subject-v3.json";
|
|
29
|
+
const EDD_V1_SUBJECT_CONTEXT =
|
|
30
|
+
"https://idos-network.github.io/idos-sdk-js/credentials/idos-credential-subject-edd-v1.json";
|
|
31
|
+
const FRONT_FILE = "<~7WNEbF8~>";
|
|
32
|
+
const PROOF_FILE = "<~:i^JmAc~>";
|
|
33
|
+
const SELFIE_FILE = "<~;e9cWBk7~>";
|
|
34
|
+
const envelopeFields = {
|
|
35
|
+
id: CRED_ID,
|
|
36
|
+
level: "human",
|
|
37
|
+
kycLevel: 1,
|
|
38
|
+
issued: new Date("2022-01-01").toISOString(),
|
|
39
|
+
approvedAt: new Date("2022-01-01").toISOString(),
|
|
40
|
+
expirationDate: new Date("2030-01-01").toISOString(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const proof = {
|
|
44
|
+
type: "Ed25519Signature2020",
|
|
45
|
+
created: new Date("2022-01-01").toISOString(),
|
|
46
|
+
verificationMethod: `${ISSUER}/keys/1`,
|
|
47
|
+
proofValue: "proof",
|
|
48
|
+
proofPurpose: "assertionMethod",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function credential<TSubject>(
|
|
52
|
+
envelopeContext: string,
|
|
53
|
+
credentialSubject: TSubject,
|
|
54
|
+
): VerifiableCredential<TSubject> & typeof envelopeFields {
|
|
55
|
+
return {
|
|
56
|
+
"@context": ["https://www.w3.org/2018/credentials/v1", envelopeContext],
|
|
57
|
+
type: ["VerifiableCredential"],
|
|
58
|
+
issuer: ISSUER,
|
|
59
|
+
...envelopeFields,
|
|
60
|
+
credentialSubject,
|
|
61
|
+
issuanceDate: envelopeFields.issued,
|
|
62
|
+
proof,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("parseCredential", () => {
|
|
67
|
+
it("returns a deserialized KYC v3 container", async () => {
|
|
68
|
+
const parsed = await parseCredential(
|
|
69
|
+
credential(CREDENTIALS_V2_CONTEXT, {
|
|
70
|
+
"@context": [KYC_V3_SUBJECT_CONTEXT],
|
|
71
|
+
id: "uuid:abc",
|
|
72
|
+
personFirstName: "John",
|
|
73
|
+
personFamilyName: "Lennon",
|
|
74
|
+
personGender: "M",
|
|
75
|
+
personNationality: "US",
|
|
76
|
+
personDateOfBirth: "1980-01-01T00:00:00.000Z",
|
|
77
|
+
personPlaceOfBirth: "New York, NY",
|
|
78
|
+
idDocumentCountry: "US",
|
|
79
|
+
idDocumentNumber: "123456789",
|
|
80
|
+
idDocumentType: "PASSPORT",
|
|
81
|
+
idDocumentDateOfIssue: "2022-01-01T00:00:00.000Z",
|
|
82
|
+
idDocumentDateOfExpiry: "2025-01-01T00:00:00.000Z",
|
|
83
|
+
idDocumentFrontFile: FRONT_FILE,
|
|
84
|
+
idDocumentFrontFileType: "image/jpeg",
|
|
85
|
+
idDocumentFrontFileName: "front.jpg",
|
|
86
|
+
contactEmail: "john@example.com",
|
|
87
|
+
contactPhoneNumber: "+1234567890",
|
|
88
|
+
biometricSelfieFile: SELFIE_FILE,
|
|
89
|
+
residentialAddressSource: "PROOF_OF_ADDRESS",
|
|
90
|
+
residentialAddressStreet: "Main St",
|
|
91
|
+
residentialAddressCity: "New York",
|
|
92
|
+
residentialAddressCountry: "US",
|
|
93
|
+
residentialAddressProofCategory: "UTILITY_BILL",
|
|
94
|
+
residentialAddressProofFile: PROOF_FILE,
|
|
95
|
+
residentialAddressProofFileType: "image/jpeg",
|
|
96
|
+
residentialAddressProofFileName: "proof.jpg",
|
|
97
|
+
screeningSanctionsCheckResult: "CLEAR",
|
|
98
|
+
screeningPepCheckResult: "CLEAR",
|
|
99
|
+
sourceOfWealthType: "SALARY",
|
|
100
|
+
sourceOfWealthYearlyGrossIncome: "LESS_THAN_20000",
|
|
101
|
+
sourceOfWealthYearlyGrossIncomeCurrency: "EUR",
|
|
102
|
+
sourceOfWealthApproximateNetWorth: "UP_TO_25000",
|
|
103
|
+
sourceOfWealthApproximateNetWorthCurrency: "EUR",
|
|
104
|
+
sourceOfWealthSourceOfWealthProofFile: PROOF_FILE,
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(parsed).toBeInstanceOf(VerifiableCredentialKycV3);
|
|
109
|
+
if (!(parsed instanceof VerifiableCredentialKycV3)) throw new Error("expected kyc v3");
|
|
110
|
+
expect(parsed.subject.id).toBe("uuid:abc");
|
|
111
|
+
expect(parsed.subject.person).toEqual({
|
|
112
|
+
firstName: "John",
|
|
113
|
+
familyName: "Lennon",
|
|
114
|
+
gender: "M",
|
|
115
|
+
nationality: "US",
|
|
116
|
+
dateOfBirth: new Date("1980-01-01T00:00:00.000Z"),
|
|
117
|
+
placeOfBirth: "New York, NY",
|
|
118
|
+
});
|
|
119
|
+
expect(parsed.subject.idDocument).toMatchObject({
|
|
120
|
+
country: "US",
|
|
121
|
+
number: "123456789",
|
|
122
|
+
type: "PASSPORT",
|
|
123
|
+
dateOfIssue: new Date("2022-01-01T00:00:00.000Z"),
|
|
124
|
+
dateOfExpiry: new Date("2025-01-01T00:00:00.000Z"),
|
|
125
|
+
});
|
|
126
|
+
expect(parsed.subject.idDocument?.frontFile?.toString()).toBe("Front");
|
|
127
|
+
expect(parsed.subject.contact).toEqual({
|
|
128
|
+
email: "john@example.com",
|
|
129
|
+
phoneNumber: "+1234567890",
|
|
130
|
+
});
|
|
131
|
+
expect(parsed.subject.biometric?.selfieFile.toString()).toBe("Selfie");
|
|
132
|
+
expect(parsed.subject.residentialAddress).toMatchObject({
|
|
133
|
+
street: "Main St",
|
|
134
|
+
city: "New York",
|
|
135
|
+
country: "US",
|
|
136
|
+
proofCategory: "UTILITY_BILL",
|
|
137
|
+
});
|
|
138
|
+
expect(parsed.subject.residentialAddress?.proofFile?.toString()).toBe("Proof");
|
|
139
|
+
expect(parsed.subject.screening).toEqual({
|
|
140
|
+
sanctionsCheckResult: "CLEAR",
|
|
141
|
+
pepCheckResult: "CLEAR",
|
|
142
|
+
});
|
|
143
|
+
expect(parsed.subject.sourceOfWealth).toMatchObject({
|
|
144
|
+
type: "SALARY",
|
|
145
|
+
yearlyGrossIncome: "LESS_THAN_20000",
|
|
146
|
+
approximateNetWorth: "UP_TO_25000",
|
|
147
|
+
});
|
|
148
|
+
expect(parsed.subject.sourceOfWealth?.sourceOfWealthProofFile?.toString()).toBe("Proof");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("returns a deserialized FaceId v1 container", async () => {
|
|
152
|
+
const parsed = await parseCredential(
|
|
153
|
+
credential(CREDENTIALS_V1_CONTEXT, {
|
|
154
|
+
"@context": [FACE_ID_V1_SUBJECT_CONTEXT],
|
|
155
|
+
faceSignUserId: "11111111-1111-1111-1111-111111111111",
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
expect(parsed).toBeInstanceOf(VerifiableCredentialFaceIdV1);
|
|
160
|
+
if (!(parsed instanceof VerifiableCredentialFaceIdV1)) throw new Error("expected faceId");
|
|
161
|
+
expect(parsed.subject.faceSignUserId).toBe("11111111-1111-1111-1111-111111111111");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns a deserialized KYC v1 container", async () => {
|
|
165
|
+
const parsed = await parseCredential(
|
|
166
|
+
credential(CREDENTIALS_V1_CONTEXT, {
|
|
167
|
+
"@context": [KYC_V1_SUBJECT_CONTEXT],
|
|
168
|
+
id: "uuid:v1",
|
|
169
|
+
firstName: "Ada",
|
|
170
|
+
idDocumentCountry: "US",
|
|
171
|
+
idDocumentNumber: "123456789",
|
|
172
|
+
idDocumentType: "PASSPORT",
|
|
173
|
+
idDocumentFrontFile: FRONT_FILE,
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(parsed).toBeInstanceOf(VerifiableCredentialKycV1);
|
|
178
|
+
if (!(parsed instanceof VerifiableCredentialKycV1)) throw new Error("expected kyc v1");
|
|
179
|
+
expect(parsed.subject.id).toBe("uuid:v1");
|
|
180
|
+
expect(parsed.subject.firstName).toBe("Ada");
|
|
181
|
+
expect(parsed.subject.idDocument).toMatchObject({
|
|
182
|
+
country: "US",
|
|
183
|
+
number: "123456789",
|
|
184
|
+
type: "PASSPORT",
|
|
185
|
+
});
|
|
186
|
+
expect(parsed.subject.idDocument?.frontFile?.toString()).toBe("Front");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("returns a deserialized KYC v2 container", async () => {
|
|
190
|
+
const parsed = await parseCredential(
|
|
191
|
+
credential(CREDENTIALS_V1_CONTEXT, {
|
|
192
|
+
"@context": [KYC_V2_SUBJECT_CONTEXT],
|
|
193
|
+
id: "uuid:v2",
|
|
194
|
+
firstName: "Grace",
|
|
195
|
+
dateOfBirth: "1906-12-09T00:00:00.000Z",
|
|
196
|
+
selfieFile: SELFIE_FILE,
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(parsed).toBeInstanceOf(VerifiableCredentialKycV2);
|
|
201
|
+
if (!(parsed instanceof VerifiableCredentialKycV2)) throw new Error("expected kyc v2");
|
|
202
|
+
expect(parsed.subject.id).toBe("uuid:v2");
|
|
203
|
+
expect(parsed.subject.firstName).toBe("Grace");
|
|
204
|
+
expect(parsed.subject.dateOfBirth).toEqual(new Date("1906-12-09T00:00:00.000Z"));
|
|
205
|
+
expect(parsed.subject.selfieFile).toEqual(Buffer.from("Selfie"));
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("returns a deserialized EDD v1 container", async () => {
|
|
209
|
+
const parsed = await parseCredential(
|
|
210
|
+
credential(CREDENTIALS_V1_CONTEXT, {
|
|
211
|
+
"@context": [EDD_V1_SUBJECT_CONTEXT],
|
|
212
|
+
id: "uuid:edd",
|
|
213
|
+
eddOccupation: "AGRICULTURE",
|
|
214
|
+
eddSourceOfFundsCategory: "SALARY",
|
|
215
|
+
eddSourceOfFundsProofFile: PROOF_FILE,
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(parsed).toBeInstanceOf(VerifiableCredentialEddV1);
|
|
220
|
+
if (!(parsed instanceof VerifiableCredentialEddV1)) throw new Error("expected edd v1");
|
|
221
|
+
expect(parsed.subject.id).toBe("uuid:edd");
|
|
222
|
+
expect(parsed.subject.edd?.occupation).toBe("AGRICULTURE");
|
|
223
|
+
expect(parsed.subject.edd?.sourceOfFundsProofFile?.toString()).toBe("Proof");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("throws for an unknown subject context", async () => {
|
|
227
|
+
await expect(
|
|
228
|
+
parseCredential(credential("unknown-envelope", { "@context": "unknown-subject" })),
|
|
229
|
+
).rejects.toThrow("Unknown credential");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("rejects a matching KYC v3 context with invalid subject data", async () => {
|
|
233
|
+
await expect(
|
|
234
|
+
parseCredential(
|
|
235
|
+
credential(CREDENTIALS_V2_CONTEXT, {
|
|
236
|
+
"@context": [KYC_V3_SUBJECT_CONTEXT],
|
|
237
|
+
id: "uuid:abc",
|
|
238
|
+
personFirstName: "John",
|
|
239
|
+
personNationality: "US",
|
|
240
|
+
personDateOfBirth: "1980-01-01T00:00:00.000Z",
|
|
241
|
+
personGender: "MALE", // invalid: enum is M | F | OTHER
|
|
242
|
+
// invalid: mandatory idDocument section is missing entirely
|
|
243
|
+
}),
|
|
244
|
+
),
|
|
245
|
+
).rejects.toThrow(CredentialValidationError);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("rejects a subject field the matched context does not define", async () => {
|
|
249
|
+
await expect(
|
|
250
|
+
parseCredential(
|
|
251
|
+
credential(CREDENTIALS_V2_CONTEXT, {
|
|
252
|
+
"@context": [KYC_V3_SUBJECT_CONTEXT],
|
|
253
|
+
id: "uuid:abc",
|
|
254
|
+
personFirstName: "John",
|
|
255
|
+
personNationality: "US",
|
|
256
|
+
personDateOfBirth: "1980-01-01T00:00:00.000Z",
|
|
257
|
+
idDocumentType: "PASSPORT",
|
|
258
|
+
idDocumentNumber: "123456789",
|
|
259
|
+
idDocumentCountry: "US",
|
|
260
|
+
idDocumentFrontFile: FRONT_FILE,
|
|
261
|
+
unexpectedJsonLdField: "breaks-json-ld",
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
).rejects.toThrow("Unexpected subject fields: unexpectedJsonLdField");
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { VerifiableCredential } from "../types";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
VerifiableCredentialEddV1,
|
|
5
|
+
VerifiableCredentialFaceIdV1,
|
|
6
|
+
VerifiableCredentialKycV1,
|
|
7
|
+
VerifiableCredentialKycV2,
|
|
8
|
+
VerifiableCredentialKycV3,
|
|
9
|
+
} from "../schemas";
|
|
10
|
+
|
|
11
|
+
type CredentialWithContext = VerifiableCredential<unknown>;
|
|
12
|
+
|
|
13
|
+
/** Every version `parseCredential` can return. */
|
|
14
|
+
export type ParsedCredential =
|
|
15
|
+
| VerifiableCredentialEddV1
|
|
16
|
+
| VerifiableCredentialFaceIdV1
|
|
17
|
+
| VerifiableCredentialKycV1
|
|
18
|
+
| VerifiableCredentialKycV2
|
|
19
|
+
| VerifiableCredentialKycV3;
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* Matched on the subject `@context`, which is unique per version — several versions share an
|
|
23
|
+
* envelope context, so that one cannot tell them apart.
|
|
24
|
+
*/
|
|
25
|
+
const VERSIONS: { readonly subjectContext: string; new (): ParsedCredential }[] = [
|
|
26
|
+
VerifiableCredentialEddV1,
|
|
27
|
+
VerifiableCredentialFaceIdV1,
|
|
28
|
+
VerifiableCredentialKycV1,
|
|
29
|
+
VerifiableCredentialKycV2,
|
|
30
|
+
VerifiableCredentialKycV3,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function hasContext(input: CredentialWithContext, context: string): boolean {
|
|
34
|
+
if (
|
|
35
|
+
typeof input.credentialSubject !== "object" ||
|
|
36
|
+
input.credentialSubject === null ||
|
|
37
|
+
Array.isArray(input.credentialSubject)
|
|
38
|
+
) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const value = (input.credentialSubject as Record<string, unknown>)["@context"];
|
|
43
|
+
return Array.isArray(value) ? value.includes(context) : value === context;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Deserializes a credential into its typed version by matching the subject `@context`, then
|
|
48
|
+
* validates it. A credential that does not match the context it claims is rejected here rather
|
|
49
|
+
* than at the first property access: a subject field the context does not define throws, and one
|
|
50
|
+
* it promises but the credential omits fails with `CredentialValidationError`.
|
|
51
|
+
*
|
|
52
|
+
* SECURITY: this does NOT verify the credential's signature. The returned version reflects
|
|
53
|
+
* whatever was in `input`, signed or forged. For untrusted input, call `verifyCredential()`
|
|
54
|
+
* (from `@idos-network/credentials/verifier`) first and only parse once it reports verified.
|
|
55
|
+
*/
|
|
56
|
+
export async function parseCredential(input: CredentialWithContext): Promise<ParsedCredential> {
|
|
57
|
+
for (const Version of VERSIONS) {
|
|
58
|
+
if (!hasContext(input, Version.subjectContext)) continue;
|
|
59
|
+
|
|
60
|
+
const credential = new Version();
|
|
61
|
+
|
|
62
|
+
await credential.deserialize(input);
|
|
63
|
+
credential.checkValidity();
|
|
64
|
+
|
|
65
|
+
return credential;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
throw new Error("Unknown credential");
|
|
69
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { base85ToFile } from "@idos-network/utils/codecs";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { EddV1 } from "./index";
|
|
5
|
+
|
|
6
|
+
describe("EddV1", () => {
|
|
7
|
+
it("serializes the edd section with its prefix", () => {
|
|
8
|
+
const credential = new EddV1()
|
|
9
|
+
.setEnvelope({ id: "https://issuer.example/credentials/edd1-123" })
|
|
10
|
+
.setSubject({ id: "https://issuer.example/credentials/edd1-123" })
|
|
11
|
+
.addSection("edd", {
|
|
12
|
+
occupation: "AGRICULTURE",
|
|
13
|
+
sourceOfFundsCategory: "BUSINESS_OWNER",
|
|
14
|
+
sourceOfFundsProofFile: Buffer.from("proof of source of funds"),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
expect(credential.level()).toBe("edd");
|
|
18
|
+
expect(credential.publicNotes()).toEqual({ type: "edd", level: "edd" });
|
|
19
|
+
|
|
20
|
+
const serialized = credential.serializeSubject();
|
|
21
|
+
expect(serialized.eddOccupation).toBe("AGRICULTURE");
|
|
22
|
+
expect(serialized.eddSourceOfFundsCategory).toBe("BUSINESS_OWNER");
|
|
23
|
+
expect(base85ToFile(serialized.eddSourceOfFundsProofFile as string)?.toString()).toBe(
|
|
24
|
+
"proof of source of funds",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("requires the edd section, and rejects an unknown occupation", () => {
|
|
29
|
+
expect(() =>
|
|
30
|
+
new EddV1()
|
|
31
|
+
.setEnvelope({ id: "https://issuer.example/credentials/edd1-123" })
|
|
32
|
+
.setSubject({ id: "uuid:abc" })
|
|
33
|
+
.checkValidity(),
|
|
34
|
+
).toThrow(/edd/);
|
|
35
|
+
|
|
36
|
+
expect(() => new EddV1().addSection("edd", { occupation: "ASTRONAUT" as never })).toThrow(
|
|
37
|
+
/occupation/,
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { IsDefined, IsIn, IsOptional, IsString } from "class-validator";
|
|
2
|
+
|
|
3
|
+
import type { PublicNotes } from "../types";
|
|
4
|
+
|
|
5
|
+
import { CONTEXT_IDOS_CREDENTIAL_V1, CONTEXT_IDOS_CREDENTIAL_V1_EDD } from "../../utils/loader";
|
|
6
|
+
import { Occupations, type Occupation } from "../enums";
|
|
7
|
+
import { EnvelopeV1 } from "../Envelope";
|
|
8
|
+
import { Base85FileField, Section } from "../utils";
|
|
9
|
+
import { VerifiableCredentialBase } from "../VerifiableCredential";
|
|
10
|
+
|
|
11
|
+
/** The same fields as KycV3's `edd` section, here as a credential of its own. */
|
|
12
|
+
export class EDD {
|
|
13
|
+
/* The person's occupation or job title. */
|
|
14
|
+
@IsOptional()
|
|
15
|
+
@IsIn(Occupations)
|
|
16
|
+
occupation?: Occupation;
|
|
17
|
+
|
|
18
|
+
/* The origin of money used in transactions (e.g. salary, business income, savings). */
|
|
19
|
+
@IsOptional()
|
|
20
|
+
@IsString()
|
|
21
|
+
sourceOfFundsCategory?: string;
|
|
22
|
+
|
|
23
|
+
/* A file containing proof of the person's source of funds. */
|
|
24
|
+
@IsOptional()
|
|
25
|
+
@Base85FileField()
|
|
26
|
+
sourceOfFundsProofFile?: Buffer;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Subject {
|
|
30
|
+
@IsString()
|
|
31
|
+
id: string;
|
|
32
|
+
|
|
33
|
+
@Section(() => EDD)
|
|
34
|
+
@IsDefined()
|
|
35
|
+
edd: EDD;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class EddV1 extends VerifiableCredentialBase<EnvelopeV1, Subject> {
|
|
39
|
+
static readonly envelopeContext: string = CONTEXT_IDOS_CREDENTIAL_V1;
|
|
40
|
+
static readonly subjectContext: string = CONTEXT_IDOS_CREDENTIAL_V1_EDD;
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
super(EnvelopeV1, Subject);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override level(): string {
|
|
47
|
+
return "edd";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override publicNotes(): PublicNotes {
|
|
51
|
+
return { type: "edd", level: this.level() };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { IsInt, IsOptional, IsPositive, IsString } from "class-validator";
|
|
2
|
+
|
|
3
|
+
import { IsoDateField } from "./utils";
|
|
4
|
+
|
|
5
|
+
/** The envelope: the credential's own fields, alongside `credentialSubject`. */
|
|
6
|
+
export class EnvelopeV1 {
|
|
7
|
+
@IsString()
|
|
8
|
+
id: string;
|
|
9
|
+
|
|
10
|
+
/* Level of KYC verification performed (e.g. basic, plus+liveness). */
|
|
11
|
+
@IsString()
|
|
12
|
+
level: string;
|
|
13
|
+
|
|
14
|
+
/* Date the credential was approved. */
|
|
15
|
+
@IsOptional()
|
|
16
|
+
@IsoDateField()
|
|
17
|
+
approvedAt?: Date;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `EnvelopeV1` plus a numeric KYC level and the issuance/expiry dates. */
|
|
21
|
+
export class EnvelopeV2 extends EnvelopeV1 {
|
|
22
|
+
/* Level of KYC verification performed (1, 2 or 3). */
|
|
23
|
+
@IsOptional()
|
|
24
|
+
@IsInt()
|
|
25
|
+
@IsPositive()
|
|
26
|
+
kycLevel?: number;
|
|
27
|
+
|
|
28
|
+
/* @default new Date() */
|
|
29
|
+
@IsOptional()
|
|
30
|
+
@IsoDateField()
|
|
31
|
+
issued?: Date;
|
|
32
|
+
|
|
33
|
+
/* Date the credential expires. */
|
|
34
|
+
@IsOptional()
|
|
35
|
+
@IsoDateField()
|
|
36
|
+
expirationDate?: Date;
|
|
37
|
+
}
|