From 2a795b767f55cfa3eec45531d5b78c71ef7240cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aybars=20G=C3=B6ktu=C4=9F=20Ayan?= Date: Wed, 20 Nov 2024 13:24:13 +0300 Subject: [PATCH 01/15] feat: nested ctype val --- .../credentials/src/V1/KiltCredentialV1.ts | 110 +++++++++++++++++- 1 file changed, 105 insertions(+), 5 deletions(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index 557322099..58144c67e 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -7,6 +7,7 @@ import { hexToU8a } from '@polkadot/util' import { base58Encode } from '@polkadot/util-crypto' +import * as Kilt from "@kiltprotocol/sdk-js" import { JsonSchema, SDKErrors } from '@kiltprotocol/utils' import type { @@ -327,15 +328,76 @@ export function fromInput({ const cachingCTypeLoader = newCachingCTypeLoader() +// Helper function to check if CType is nested +function cTypeTypeFinder(cType: ICType): boolean { + function hasRef(obj: any): boolean { + if (typeof obj !== 'object' || obj === null) return false + + if ('$ref' in obj) return true + + return Object.values(obj).some(value => { + if (Array.isArray(value)) { + return value.some(item => hasRef(item)) + } + if (typeof value === 'object') { + return hasRef(value) + } + return false + }) + } + return hasRef(cType.properties) +} + +// Helper function to extract unique references from CType +function extractUniqueReferences(cType: ICType): Set { + const references = new Set() + + function processValue(value: any) { + if (typeof value !== 'object' || value === null) return + + if ('$ref' in value) { + const ref = value['$ref'] + // Extract KILT CType reference + if (ref.startsWith('kilt:ctype:')) { + // Get first part split by #/ + const baseRef = ref.split('#/')[0] + references.add(baseRef) + } + } + + // Check all values of the object recursively + Object.values(value).forEach(v => processValue(v)) + } + + processValue(cType.properties) + return references +} + + /** * Validates the claims in the VC's `credentialSubject` against a CType definition. + * Supports both nested and non-nested CType validation. + * + * For non-nested CTypes: + * - Validates claims directly against the CType schema + * + * For nested CTypes: + * - Automatically detects nested structure through $ref properties + * - Fetches referenced CTypes from the blockchain + * - Performs validation against the main CType and all referenced CTypes * * @param credential A {@link KiltCredentialV1} type verifiable credential. * @param credential.credentialSubject The credentialSubject to be validated. * @param credential.type The credential's types. * @param options Options map. - * @param options.cTypes One or more CType definitions to be used for validation. If `loadCTypes` is set to `false`, validation will fail if the definition of the credential's CType is not given. - * @param options.loadCTypes A function to load CType definitions that are not in `cTypes`. Defaults to using the {@link newCachingCTypeLoader | CachingCTypeLoader}. If set to `false` or `undefined`, no additional CTypes will be loaded. + * @param options.cTypes One or more CType definitions to be used for validation. If loadCTypes is set to false, validation will fail if the definition of the credential's CType is not given. + * @param options.loadCTypes A function to load CType definitions that are not in cTypes. Defaults to using the {@link newCachingCTypeLoader | CachingCTypeLoader}. If set to false or undefined, no additional CTypes will be loaded. + * + * @throws {Error} If the credential type does not contain a valid CType id + * @throws {Error} If required CType definitions cannot be loaded + * @throws {Error} If claims do not follow the expected CType format + * @throws {Error} If referenced CTypes in nested structure cannot be fetched from the blockchain + * @throws {Error} If validation fails against the CType schema */ export async function validateSubject( { @@ -344,7 +406,7 @@ export async function validateSubject( }: Pick, { cTypes = [], - loadCTypes = cachingCTypeLoader, + loadCTypes = newCachingCTypeLoader(), }: { cTypes?: ICType[]; loadCTypes?: false | CTypeLoader } = {} ): Promise { // get CType id referenced in credential @@ -354,6 +416,7 @@ export async function validateSubject( if (!credentialsCTypeId) { throw new Error('credential type does not contain a valid CType id') } + // check that we have access to the right schema let cType = cTypes?.find(({ $id }) => $id === credentialsCTypeId) if (!cType) { @@ -385,6 +448,43 @@ export async function validateSubject( [key.substring(vocab.length)]: value, } }, {}) - // validates against CType (also validates CType schema itself) - CType.verifyClaimAgainstSchema(claims, cType) + + // Connect to blockchain + const api = Kilt.ConfigService.get('api') + + // Bizim eklediğimiz doğrulama mantığı + const isNested = cTypeTypeFinder(cType) + + if (!isNested) { + await CType.verifyClaimAgainstNestedSchemas( + cType, + [], + claims + ) + } else { + const references = extractUniqueReferences(cType) + const referencedCTypes: ICType[] = [] + + for (const ref of references) { + try { + const referencedCType = await CType.fetchFromChain(ref as any) + if (referencedCType.cType) { + referencedCTypes.push(referencedCType.cType) + } + } catch (error) { + console.error(`Failed to fetch CType for reference ${ref}:`, error) + throw new Error(`Failed to fetch CType from chain: ${ref}`) + } + } + + if (referencedCTypes.length === references.size) { + await CType.verifyClaimAgainstNestedSchemas( + cType, + referencedCTypes, + claims + ) + } else { + throw new Error("Some referenced CTypes could not be fetched") + } + } } From 77b9c3d89f213c51b6b428ade650cc04c50efc2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aybars=20G=C3=B6ktu=C4=9F=20Ayan?= Date: Mon, 2 Dec 2024 22:33:34 +0300 Subject: [PATCH 02/15] fix: review changes --- .../credentials/src/V1/KiltCredentialV1.ts | 133 +++++++++--------- 1 file changed, 68 insertions(+), 65 deletions(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index 58144c67e..a4d85eb3a 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -7,7 +7,6 @@ import { hexToU8a } from '@polkadot/util' import { base58Encode } from '@polkadot/util-crypto' -import * as Kilt from "@kiltprotocol/sdk-js" import { JsonSchema, SDKErrors } from '@kiltprotocol/utils' import type { @@ -328,47 +327,53 @@ export function fromInput({ const cachingCTypeLoader = newCachingCTypeLoader() -// Helper function to check if CType is nested -function cTypeTypeFinder(cType: ICType): boolean { - function hasRef(obj: any): boolean { - if (typeof obj !== 'object' || obj === null) return false - - if ('$ref' in obj) return true - - return Object.values(obj).some(value => { - if (Array.isArray(value)) { - return value.some(item => hasRef(item)) - } - if (typeof value === 'object') { - return hasRef(value) - } - return false - }) +// Check recursively if a value has references +const hasRef = (value: unknown): boolean => { + if (typeof value !== 'object' || value === null) { + return false + } + + if ('$ref' in (value as Record)) { + return true + } + + if (Array.isArray(value)) { + return value.some(item => hasRef(item)) } - return hasRef(cType.properties) + + return Object.values(value as Record).some(v => hasRef(v)) } -// Helper function to extract unique references from CType -function extractUniqueReferences(cType: ICType): Set { - const references = new Set() - - function processValue(value: any) { - if (typeof value !== 'object' || value === null) return - - if ('$ref' in value) { - const ref = value['$ref'] - // Extract KILT CType reference - if (ref.startsWith('kilt:ctype:')) { - // Get first part split by #/ - const baseRef = ref.split('#/')[0] - references.add(baseRef) - } +// Single function to both check for references and extract them +function extractUniqueReferences( + cType: ICType, + references: Set = new Set() +): Set { + if (typeof cType?.properties !== 'object' || cType.properties === null) { + return references + } + + const processValue = (value: unknown): void => { + if (typeof value !== 'object' || value === null) { + return + } + + const objValue = value as Record + + if ('$ref' in objValue) { + const ref = objValue['$ref'] as string + if (ref.startsWith('kilt:ctype:')) { + references.add(ref.split('#/')[0]) } - - // Check all values of the object recursively - Object.values(value).forEach(v => processValue(v)) + } + + if (Array.isArray(value)) { + value.forEach(processValue) + } else { + Object.values(objValue).forEach(processValue) + } } - + processValue(cType.properties) return references } @@ -406,7 +411,7 @@ export async function validateSubject( }: Pick, { cTypes = [], - loadCTypes = newCachingCTypeLoader(), + loadCTypes = cachingCTypeLoader, }: { cTypes?: ICType[]; loadCTypes?: false | CTypeLoader } = {} ): Promise { // get CType id referenced in credential @@ -448,43 +453,41 @@ export async function validateSubject( [key.substring(vocab.length)]: value, } }, {}) - - // Connect to blockchain - const api = Kilt.ConfigService.get('api') - - // Bizim eklediğimiz doğrulama mantığı - const isNested = cTypeTypeFinder(cType) - if (!isNested) { - await CType.verifyClaimAgainstNestedSchemas( - cType, - [], - claims - ) - } else { + try { + // Find references - if none exist, will return empty Set const references = extractUniqueReferences(cType) - const referencedCTypes: ICType[] = [] - for (const ref of references) { - try { - const referencedCType = await CType.fetchFromChain(ref as any) - if (referencedCType.cType) { - referencedCTypes.push(referencedCType.cType) + // Load referenced CTypes in parallel - if no references, will be empty array + const referencedCTypes = await Promise.all( + Array.from(references).map(async (ref) => { + try { + const referencedCType = await cachingCTypeLoader(ref as any) + return referencedCType + } catch (error) { + console.error(`Failed to fetch CType for reference ${ref}:`, error) + throw error } - } catch (error) { - console.error(`Failed to fetch CType for reference ${ref}:`, error) - throw new Error(`Failed to fetch CType from chain: ${ref}`) - } - } + }) + ) - if (referencedCTypes.length === references.size) { + // Filter out any undefined or null values + const validCTypes = referencedCTypes.filter((ctype): ctype is ICType => + ctype !== undefined && ctype !== null + ) + + // Verify if all CTypes were fetched successfully + if (validCTypes.length === references.size) { await CType.verifyClaimAgainstNestedSchemas( cType, - referencedCTypes, + validCTypes, claims ) } else { throw new Error("Some referenced CTypes could not be fetched") } + } catch (error) { + console.error("Validation error:", error) + throw error } -} +} \ No newline at end of file From 9e2596eb270bb5e24e0008e6e76e43935ce3e5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aybars=20G=C3=B6ktu=C4=9F=20Ayan?= Date: Mon, 2 Dec 2024 22:46:26 +0300 Subject: [PATCH 03/15] fix: linter added --- .../credentials/src/V1/KiltCredentialV1.ts | 61 ++++++++----------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index a4d85eb3a..fce59363b 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -338,15 +338,15 @@ const hasRef = (value: unknown): boolean => { } if (Array.isArray(value)) { - return value.some(item => hasRef(item)) + return value.some((item) => hasRef(item)) } - return Object.values(value as Record).some(v => hasRef(v)) + return Object.values(value as Record).some((v) => hasRef(v)) } // Single function to both check for references and extract them function extractUniqueReferences( - cType: ICType, + cType: ICType, references: Set = new Set() ): Set { if (typeof cType?.properties !== 'object' || cType.properties === null) { @@ -357,11 +357,9 @@ function extractUniqueReferences( if (typeof value !== 'object' || value === null) { return } - - const objValue = value as Record - + const objValue = value as Record if ('$ref' in objValue) { - const ref = objValue['$ref'] as string + const ref = objValue.$ref as string if (ref.startsWith('kilt:ctype:')) { references.add(ref.split('#/')[0]) } @@ -378,18 +376,15 @@ function extractUniqueReferences( return references } - /** * Validates the claims in the VC's `credentialSubject` against a CType definition. * Supports both nested and non-nested CType validation. - * * For non-nested CTypes: - * - Validates claims directly against the CType schema - * + * - Validates claims directly against the CType schema. * For nested CTypes: - * - Automatically detects nested structure through $ref properties - * - Fetches referenced CTypes from the blockchain - * - Performs validation against the main CType and all referenced CTypes + * - Automatically detects nested structure through $ref properties. + * - Fetches referenced CTypes from the blockchain. + * - Performs validation against the main CType and all referenced CTypes. * * @param credential A {@link KiltCredentialV1} type verifiable credential. * @param credential.credentialSubject The credentialSubject to be validated. @@ -397,12 +392,12 @@ function extractUniqueReferences( * @param options Options map. * @param options.cTypes One or more CType definitions to be used for validation. If loadCTypes is set to false, validation will fail if the definition of the credential's CType is not given. * @param options.loadCTypes A function to load CType definitions that are not in cTypes. Defaults to using the {@link newCachingCTypeLoader | CachingCTypeLoader}. If set to false or undefined, no additional CTypes will be loaded. - * - * @throws {Error} If the credential type does not contain a valid CType id - * @throws {Error} If required CType definitions cannot be loaded - * @throws {Error} If claims do not follow the expected CType format - * @throws {Error} If referenced CTypes in nested structure cannot be fetched from the blockchain - * @throws {Error} If validation fails against the CType schema + * + * @throws {Error} If the credential type does not contain a valid CType id. + * @throws {Error} If required CType definitions cannot be loaded. + * @throws {Error} If claims do not follow the expected CType format. + * @throws {Error} If referenced CTypes in nested structure cannot be fetched from the blockchain. + * @throws {Error} If validation fails against the CType schema. */ export async function validateSubject( { @@ -453,41 +448,35 @@ export async function validateSubject( [key.substring(vocab.length)]: value, } }, {}) - + try { - // Find references - if none exist, will return empty Set const references = extractUniqueReferences(cType) - - // Load referenced CTypes in parallel - if no references, will be empty array + const referencedCTypes = await Promise.all( Array.from(references).map(async (ref) => { try { const referencedCType = await cachingCTypeLoader(ref as any) return referencedCType } catch (error) { + // eslint-disable-next-line no-console console.error(`Failed to fetch CType for reference ${ref}:`, error) throw error } }) ) - // Filter out any undefined or null values - const validCTypes = referencedCTypes.filter((ctype): ctype is ICType => - ctype !== undefined && ctype !== null + const validCTypes = referencedCTypes.filter( + (ctype): ctype is ICType => ctype !== undefined && ctype !== null ) - // Verify if all CTypes were fetched successfully if (validCTypes.length === references.size) { - await CType.verifyClaimAgainstNestedSchemas( - cType, - validCTypes, - claims - ) + await CType.verifyClaimAgainstNestedSchemas(cType, validCTypes, claims) } else { - throw new Error("Some referenced CTypes could not be fetched") + throw new Error('Some referenced CTypes could not be fetched') } } catch (error) { - console.error("Validation error:", error) + // eslint-disable-next-line no-console + console.error('Validation error:', error) throw error } -} \ No newline at end of file +} From d8513816708fb33baefbd6750b100779ad886b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aybars=20G=C3=B6ktu=C4=9F=20Ayan?= Date: Mon, 2 Dec 2024 22:51:00 +0300 Subject: [PATCH 04/15] fix linter2 --- packages/credentials/src/V1/KiltCredentialV1.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index fce59363b..81a38a632 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -357,7 +357,7 @@ function extractUniqueReferences( if (typeof value !== 'object' || value === null) { return } - const objValue = value as Record + const objValue = value as Record if ('$ref' in objValue) { const ref = objValue.$ref as string if (ref.startsWith('kilt:ctype:')) { From bbfced24e1897748e57f40ddc5fa06267ed4962c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aybars=20G=C3=B6ktu=C4=9F=20Ayan?= Date: Tue, 3 Dec 2024 17:00:24 +0300 Subject: [PATCH 05/15] fix: review2 --- .../credentials/src/V1/KiltCredentialV1.ts | 101 +++++++----------- 1 file changed, 37 insertions(+), 64 deletions(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index 81a38a632..a927ee100 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -327,23 +327,6 @@ export function fromInput({ const cachingCTypeLoader = newCachingCTypeLoader() -// Check recursively if a value has references -const hasRef = (value: unknown): boolean => { - if (typeof value !== 'object' || value === null) { - return false - } - - if ('$ref' in (value as Record)) { - return true - } - - if (Array.isArray(value)) { - return value.some((item) => hasRef(item)) - } - - return Object.values(value as Record).some((v) => hasRef(v)) -} - // Single function to both check for references and extract them function extractUniqueReferences( cType: ICType, @@ -353,26 +336,27 @@ function extractUniqueReferences( return references } - const processValue = (value: unknown): void => { - if (typeof value !== 'object' || value === null) { - return - } - const objValue = value as Record - if ('$ref' in objValue) { - const ref = objValue.$ref as string - if (ref.startsWith('kilt:ctype:')) { - references.add(ref.split('#/')[0]) - } - } + const objValue = cType.properties as Record - if (Array.isArray(value)) { - value.forEach(processValue) - } else { - Object.values(objValue).forEach(processValue) + if ('$ref' in objValue) { + const ref = objValue.$ref as string + if (ref.startsWith('kilt:ctype:')) { + references.add(ref.split('#/')[0]) } } - processValue(cType.properties) + if (Array.isArray(objValue)) { + objValue.forEach((item) => + extractUniqueReferences(item as ICType, references) + ) + } else { + Object.values(objValue).forEach((value) => { + if (typeof value === 'object' && value !== null) { + extractUniqueReferences(value as ICType, references) + } + }) + } + return references } @@ -409,7 +393,6 @@ export async function validateSubject( loadCTypes = cachingCTypeLoader, }: { cTypes?: ICType[]; loadCTypes?: false | CTypeLoader } = {} ): Promise { - // get CType id referenced in credential const credentialsCTypeId = type.find((str) => str.startsWith('kilt:ctype:') ) as ICType['$id'] @@ -417,7 +400,6 @@ export async function validateSubject( throw new Error('credential type does not contain a valid CType id') } - // check that we have access to the right schema let cType = cTypes?.find(({ $id }) => $id === credentialsCTypeId) if (!cType) { if (typeof loadCTypes !== 'function') { @@ -431,7 +413,6 @@ export async function validateSubject( } } - // normalize credential subject to form expected by CType schema const expandedClaims: Record = jsonLdExpandCredentialSubject(credentialSubject) delete expandedClaims['@id'] @@ -449,34 +430,26 @@ export async function validateSubject( } }, {}) - try { - const references = extractUniqueReferences(cType) - - const referencedCTypes = await Promise.all( - Array.from(references).map(async (ref) => { - try { - const referencedCType = await cachingCTypeLoader(ref as any) - return referencedCType - } catch (error) { - // eslint-disable-next-line no-console - console.error(`Failed to fetch CType for reference ${ref}:`, error) - throw error - } - }) - ) + const references = extractUniqueReferences(cType) - const validCTypes = referencedCTypes.filter( - (ctype): ctype is ICType => ctype !== undefined && ctype !== null - ) - - if (validCTypes.length === references.size) { - await CType.verifyClaimAgainstNestedSchemas(cType, validCTypes, claims) - } else { - throw new Error('Some referenced CTypes could not be fetched') - } - } catch (error) { - // eslint-disable-next-line no-console - console.error('Validation error:', error) - throw error + const referencedCTypes = await Promise.all( + Array.from(references).map(async (ref) => { + if (typeof loadCTypes !== 'function') { + throw new Error( + `The definition for this credential's CType ${ref} has not been passed to the validator and CType loading has been disabled` + ) + } + return loadCTypes(ref as any) + }) + ) + + const validCTypes = referencedCTypes.filter( + (ctype): ctype is ICType => ctype !== undefined && ctype !== null + ) + + if (validCTypes.length === references.size) { + await CType.verifyClaimAgainstNestedSchemas(cType, validCTypes, claims) + } else { + throw new Error('Some referenced CTypes could not be fetched') } } From a97c06645bfa3f8a18f7e47010b0d5a2e570a55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aybars=20G=C3=B6ktu=C4=9F=20Ayan?= Date: Mon, 16 Dec 2024 22:46:44 +0300 Subject: [PATCH 06/15] fix: infinite nest --- .../credentials/src/V1/KiltCredentialV1.ts | 88 +++++++++++-------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index a927ee100..9fb2e2003 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -327,37 +327,59 @@ export function fromInput({ const cachingCTypeLoader = newCachingCTypeLoader() -// Single function to both check for references and extract them -function extractUniqueReferences( +async function loadNestedCTypeDefinitions( cType: ICType, - references: Set = new Set() -): Set { - if (typeof cType?.properties !== 'object' || cType.properties === null) { - return references - } + cTypeLoader: (id: string) => Promise +): Promise> { + const fetchedCTypeIds = new Set() + const fetchedCTypeDefinitions = new Set() - const objValue = cType.properties as Record + async function processValue(value: unknown): Promise { + if (typeof value !== 'object' || value === null) { + return + } - if ('$ref' in objValue) { - const ref = objValue.$ref as string - if (ref.startsWith('kilt:ctype:')) { - references.add(ref.split('#/')[0]) + if (Array.isArray(value)) { + await Promise.all(value.map(processValue)) + return } - } - if (Array.isArray(objValue)) { - objValue.forEach((item) => - extractUniqueReferences(item as ICType, references) - ) - } else { - Object.values(objValue).forEach((value) => { - if (typeof value === 'object' && value !== null) { - extractUniqueReferences(value as ICType, references) + // Check if value is an object with $ref + const objValue = value as Record + if ('$ref' in objValue) { + const ref = objValue.$ref + if (typeof ref === 'string' && ref.startsWith('kilt:ctype:')) { + const cTypeId = ref.split('#/')[0] + + if (!fetchedCTypeIds.has(cTypeId)) { + fetchedCTypeIds.add(cTypeId) + const referencedCType = await cTypeLoader(cTypeId) + + if (referencedCType === undefined || referencedCType === null) { + throw new Error(`Failed to load referenced CType: ${cTypeId}`) + } + + fetchedCTypeDefinitions.add(referencedCType) + + const { properties } = referencedCType + if (properties !== undefined && properties !== null) { + await Promise.all(Object.values(properties).map(processValue)) + } + } } - }) + return + } + + // Process all values in the object + await Promise.all(Object.values(objValue).map(processValue)) } - return references + const { properties } = cType + if (properties !== undefined && properties !== null) { + await Promise.all(Object.values(properties).map(processValue)) + } + + return fetchedCTypeDefinitions } /** @@ -430,24 +452,16 @@ export async function validateSubject( } }, {}) - const references = extractUniqueReferences(cType) - - const referencedCTypes = await Promise.all( - Array.from(references).map(async (ref) => { - if (typeof loadCTypes !== 'function') { - throw new Error( - `The definition for this credential's CType ${ref} has not been passed to the validator and CType loading has been disabled` - ) - } - return loadCTypes(ref as any) - }) - ) + // Load all nested CTypes + const referencedCTypes = await loadNestedCTypeDefinitions(cType, loadCTypes) - const validCTypes = referencedCTypes.filter( + // Convert Set to Array and filter out any undefined or null values + const validCTypes = Array.from(referencedCTypes).filter( (ctype): ctype is ICType => ctype !== undefined && ctype !== null ) - if (validCTypes.length === references.size) { + // Verify if all CTypes were fetched successfully + if (validCTypes.length === referencedCTypes.size) { await CType.verifyClaimAgainstNestedSchemas(cType, validCTypes, claims) } else { throw new Error('Some referenced CTypes could not be fetched') From 1adad9da8f6a89a7e21098b4ace1315926cd59c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aybars=20G=C3=B6ktu=C4=9F=20Ayan?= Date: Thu, 19 Dec 2024 13:04:58 +0300 Subject: [PATCH 07/15] fix: effective loader --- .../credentials/src/V1/KiltCredentialV1.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index 9fb2e2003..4cc2d5c09 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -452,8 +452,31 @@ export async function validateSubject( } }, {}) + // Create a type-safe loader function + const effectiveLoader = async (id: string): Promise => { + // Ensure the ID is in the correct format + if (!id.startsWith('kilt:ctype:0x')) { + throw new Error(`Invalid CType ID format: ${id}`) + } + + const typedId = id as `kilt:ctype:0x${string}` + + if (typeof loadCTypes === 'function') { + return loadCTypes(typedId) + } + const found = cTypes.find((ct) => ct.$id === typedId) + if (found) { + return found + } + throw new Error( + `CType ${id} not found in provided cTypes array and CType loading is disabled` + ) + } // Load all nested CTypes - const referencedCTypes = await loadNestedCTypeDefinitions(cType, loadCTypes) + const referencedCTypes = await loadNestedCTypeDefinitions( + cType, + effectiveLoader + ) // Convert Set to Array and filter out any undefined or null values const validCTypes = Array.from(referencedCTypes).filter( From b0fbaf236de0c534af22d38fcf418d4017e66e96 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 17 Feb 2025 14:51:13 +0200 Subject: [PATCH 08/15] refactor: simplify loadNestedCTypeDefinitions --- .../credentials/src/V1/KiltCredentialV1.ts | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index 4cc2d5c09..908d912be 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -329,55 +329,44 @@ const cachingCTypeLoader = newCachingCTypeLoader() async function loadNestedCTypeDefinitions( cType: ICType, - cTypeLoader: (id: string) => Promise + cTypeLoader: CTypeLoader ): Promise> { const fetchedCTypeIds = new Set() const fetchedCTypeDefinitions = new Set() - async function processValue(value: unknown): Promise { + async function extractRefsFrom(value: unknown): Promise { if (typeof value !== 'object' || value === null) { return } - if (Array.isArray(value)) { - await Promise.all(value.map(processValue)) - return - } - - // Check if value is an object with $ref - const objValue = value as Record - if ('$ref' in objValue) { - const ref = objValue.$ref + if ('$ref' in value) { + const ref = (value as { $ref: unknown }).$ref if (typeof ref === 'string' && ref.startsWith('kilt:ctype:')) { - const cTypeId = ref.split('#/')[0] + const cTypeId = ref.split('#/')[0] as ICType['$id'] if (!fetchedCTypeIds.has(cTypeId)) { fetchedCTypeIds.add(cTypeId) const referencedCType = await cTypeLoader(cTypeId) - if (referencedCType === undefined || referencedCType === null) { + CType.isICType(referencedCType) + + if (CType.isICType(referencedCType)) { + fetchedCTypeDefinitions.add(referencedCType) + } else { throw new Error(`Failed to load referenced CType: ${cTypeId}`) } - fetchedCTypeDefinitions.add(referencedCType) - - const { properties } = referencedCType - if (properties !== undefined && properties !== null) { - await Promise.all(Object.values(properties).map(processValue)) - } + await extractRefsFrom(referencedCType.properties) } } return } - // Process all values in the object - await Promise.all(Object.values(objValue).map(processValue)) + // Process all values in the object. Also works for arrays + await Promise.all(Object.values(value).map(extractRefsFrom)) } - const { properties } = cType - if (properties !== undefined && properties !== null) { - await Promise.all(Object.values(properties).map(processValue)) - } + await extractRefsFrom(cType.properties) return fetchedCTypeDefinitions } @@ -453,7 +442,7 @@ export async function validateSubject( }, {}) // Create a type-safe loader function - const effectiveLoader = async (id: string): Promise => { + const effectiveLoader: CTypeLoader = async (id) => { // Ensure the ID is in the correct format if (!id.startsWith('kilt:ctype:0x')) { throw new Error(`Invalid CType ID format: ${id}`) From bff81a90f3a94d70fc6a8369e7169a49acae831a Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 17 Feb 2025 17:26:41 +0200 Subject: [PATCH 09/15] refactor: streamline ctype loading --- .../credentials/src/V1/KiltCredentialV1.ts | 87 ++++++++++--------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index 908d912be..6344640ef 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -371,6 +371,34 @@ async function loadNestedCTypeDefinitions( return fetchedCTypeDefinitions } +function combineCTypeLoaders( + ...loaders: Array +): CTypeLoader { + const validLoaders = loaders.filter( + (l): l is CTypeLoader => typeof l === 'function' + ) + + return async (id) => { + // Ensure the ID is in the correct format + CType.idToHash(id) + + // eslint-disable-next-line no-restricted-syntax + for (const loadCTypes of validLoaders) { + try { + // eslint-disable-next-line no-await-in-loop + const cType = await loadCTypes(id) + if (CType.isICType(cType)) { + return cType + } + } catch { + // noop + } + } + + throw new Error(`Unable to load CType ${id}`) + } +} + /** * Validates the claims in the VC's `credentialSubject` against a CType definition. * Supports both nested and non-nested CType validation. @@ -411,24 +439,11 @@ export async function validateSubject( throw new Error('credential type does not contain a valid CType id') } - let cType = cTypes?.find(({ $id }) => $id === credentialsCTypeId) - if (!cType) { - if (typeof loadCTypes !== 'function') { - throw new Error( - `The definition for this credential's CType ${credentialsCTypeId} has not been passed to the validator and CType loading has been disabled` - ) - } - cType = await loadCTypes(credentialsCTypeId) - if (cType.$id !== credentialsCTypeId) { - throw new Error('failed to load correct CType') - } - } - const expandedClaims: Record = jsonLdExpandCredentialSubject(credentialSubject) delete expandedClaims['@id'] - const vocab = `${cType.$id}#` + const vocab = `${credentialsCTypeId}#` const claims = Object.entries(expandedClaims).reduce((obj, [key, value]) => { if (!key.startsWith(vocab)) { throw new Error( @@ -441,41 +456,33 @@ export async function validateSubject( } }, {}) - // Create a type-safe loader function - const effectiveLoader: CTypeLoader = async (id) => { - // Ensure the ID is in the correct format - if (!id.startsWith('kilt:ctype:0x')) { - throw new Error(`Invalid CType ID format: ${id}`) - } + const loaderFromCTypes: CTypeLoader | undefined = + cTypes?.length > 0 + ? ((async (id) => cTypes.find(({ $id }) => $id === id)) as CTypeLoader) + : undefined - const typedId = id as `kilt:ctype:0x${string}` + // Turn CType loader & ctypes array into combined loader function + const combinedCTypeLoader = combineCTypeLoaders(loaderFromCTypes, loadCTypes) - if (typeof loadCTypes === 'function') { - return loadCTypes(typedId) - } - const found = cTypes.find((ct) => ct.$id === typedId) - if (found) { - return found - } + const cType = await combinedCTypeLoader(credentialsCTypeId).catch(() => { throw new Error( - `CType ${id} not found in provided cTypes array and CType loading is disabled` + `The definition for this credential's CType ${credentialsCTypeId} has not been passed to the validator and could not be loaded either` ) + }) + + if (cType.$id !== credentialsCTypeId) { + throw new Error('failed to load correct CType') } + // Load all nested CTypes const referencedCTypes = await loadNestedCTypeDefinitions( cType, - effectiveLoader + combinedCTypeLoader ) - // Convert Set to Array and filter out any undefined or null values - const validCTypes = Array.from(referencedCTypes).filter( - (ctype): ctype is ICType => ctype !== undefined && ctype !== null + CType.verifyClaimAgainstNestedSchemas( + cType, + Array.from(referencedCTypes), + claims ) - - // Verify if all CTypes were fetched successfully - if (validCTypes.length === referencedCTypes.size) { - await CType.verifyClaimAgainstNestedSchemas(cType, validCTypes, claims) - } else { - throw new Error('Some referenced CTypes could not be fetched') - } } From 6f6ca9760aaf505c95261bb05174b01be66d9f39 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 17 Feb 2025 17:51:53 +0200 Subject: [PATCH 10/15] docs: restore some docstrings, update others --- packages/credentials/src/V1/KiltCredentialV1.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index 6344640ef..c5a5cf0ec 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -405,22 +405,16 @@ function combineCTypeLoaders( * For non-nested CTypes: * - Validates claims directly against the CType schema. * For nested CTypes: - * - Automatically detects nested structure through $ref properties. - * - Fetches referenced CTypes from the blockchain. + * - Automatically detects nested structure through `$ref` properties. + * - Fetches referenced CTypes via the `loadCTypes` funtion, if not included in `cTypes`. * - Performs validation against the main CType and all referenced CTypes. * * @param credential A {@link KiltCredentialV1} type verifiable credential. * @param credential.credentialSubject The credentialSubject to be validated. * @param credential.type The credential's types. * @param options Options map. - * @param options.cTypes One or more CType definitions to be used for validation. If loadCTypes is set to false, validation will fail if the definition of the credential's CType is not given. - * @param options.loadCTypes A function to load CType definitions that are not in cTypes. Defaults to using the {@link newCachingCTypeLoader | CachingCTypeLoader}. If set to false or undefined, no additional CTypes will be loaded. - * - * @throws {Error} If the credential type does not contain a valid CType id. - * @throws {Error} If required CType definitions cannot be loaded. - * @throws {Error} If claims do not follow the expected CType format. - * @throws {Error} If referenced CTypes in nested structure cannot be fetched from the blockchain. - * @throws {Error} If validation fails against the CType schema. + * @param options.cTypes One or more CType definitions to be used for validation. If `loadCTypes` is set to `false`, validation will fail if the definition of the credential's CType is not given. + * @param options.loadCTypes A function to load CType definitions that are not in `cTypes`. Defaults to using the {@link newCachingCTypeLoader | CachingCTypeLoader}. If set to `false` or `undefined`, no additional CTypes will be loaded. */ export async function validateSubject( { @@ -432,6 +426,7 @@ export async function validateSubject( loadCTypes = cachingCTypeLoader, }: { cTypes?: ICType[]; loadCTypes?: false | CTypeLoader } = {} ): Promise { + // get CType id referenced in credential const credentialsCTypeId = type.find((str) => str.startsWith('kilt:ctype:') ) as ICType['$id'] @@ -439,6 +434,7 @@ export async function validateSubject( throw new Error('credential type does not contain a valid CType id') } + // normalize credential subject to form expected by CType schema const expandedClaims: Record = jsonLdExpandCredentialSubject(credentialSubject) delete expandedClaims['@id'] @@ -480,6 +476,7 @@ export async function validateSubject( combinedCTypeLoader ) + // validates against CType (also validates CType schema itself) CType.verifyClaimAgainstNestedSchemas( cType, Array.from(referencedCTypes), From 7751f5be053a9267ea2fd9c0eec365736c8812f0 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 17 Feb 2025 17:52:28 +0200 Subject: [PATCH 11/15] test: add unit test --- .../src/V1/KiltCredentialV1.spec.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/credentials/src/V1/KiltCredentialV1.spec.ts b/packages/credentials/src/V1/KiltCredentialV1.spec.ts index 06ee1fa84..d849cf3bf 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.spec.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.spec.ts @@ -14,6 +14,7 @@ import { } from '../../../../tests/testUtils/testData.js' import { credentialSchema, + fromInput, validateStructure, validateSubject, } from './KiltCredentialV1.js' @@ -32,6 +33,28 @@ it('it verifies valid claim against schema', async () => { await expect(validateSubject(VC, { cTypes: [cType] })).resolves.not.toThrow() }) +it('it verifies valid claim against nested schema', async () => { + const nestedCType = CType.fromProperties('nested', { + prop: { + $ref: cType.$id, + }, + }) + const nestedVc = fromInput({ + cType: nestedCType.$id, + claims: { + prop: { + name: 'Kurt', + }, + }, + subject: VC.credentialSubject.id, + issuer: VC.issuer, + }) + + await expect( + validateSubject(nestedVc, { cTypes: [nestedCType, cType] }) + ).resolves.not.toThrow() +}) + it('it detects schema violations', async () => { const credentialSubject = { ...VC.credentialSubject, name: 5 } await expect( From 304844e69f1222d5bf2da2c43ab5875f3152be93 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 17 Feb 2025 18:25:25 +0200 Subject: [PATCH 12/15] refactor: move new functions to CTypeLoader.ts --- .../credentials/src/V1/KiltCredentialV1.ts | 88 +++---------------- packages/credentials/src/ctype/CTypeLoader.ts | 72 +++++++++++++++ 2 files changed, 82 insertions(+), 78 deletions(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index c5a5cf0ec..08e06ccbf 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -327,78 +327,6 @@ export function fromInput({ const cachingCTypeLoader = newCachingCTypeLoader() -async function loadNestedCTypeDefinitions( - cType: ICType, - cTypeLoader: CTypeLoader -): Promise> { - const fetchedCTypeIds = new Set() - const fetchedCTypeDefinitions = new Set() - - async function extractRefsFrom(value: unknown): Promise { - if (typeof value !== 'object' || value === null) { - return - } - - if ('$ref' in value) { - const ref = (value as { $ref: unknown }).$ref - if (typeof ref === 'string' && ref.startsWith('kilt:ctype:')) { - const cTypeId = ref.split('#/')[0] as ICType['$id'] - - if (!fetchedCTypeIds.has(cTypeId)) { - fetchedCTypeIds.add(cTypeId) - const referencedCType = await cTypeLoader(cTypeId) - - CType.isICType(referencedCType) - - if (CType.isICType(referencedCType)) { - fetchedCTypeDefinitions.add(referencedCType) - } else { - throw new Error(`Failed to load referenced CType: ${cTypeId}`) - } - - await extractRefsFrom(referencedCType.properties) - } - } - return - } - - // Process all values in the object. Also works for arrays - await Promise.all(Object.values(value).map(extractRefsFrom)) - } - - await extractRefsFrom(cType.properties) - - return fetchedCTypeDefinitions -} - -function combineCTypeLoaders( - ...loaders: Array -): CTypeLoader { - const validLoaders = loaders.filter( - (l): l is CTypeLoader => typeof l === 'function' - ) - - return async (id) => { - // Ensure the ID is in the correct format - CType.idToHash(id) - - // eslint-disable-next-line no-restricted-syntax - for (const loadCTypes of validLoaders) { - try { - // eslint-disable-next-line no-await-in-loop - const cType = await loadCTypes(id) - if (CType.isICType(cType)) { - return cType - } - } catch { - // noop - } - } - - throw new Error(`Unable to load CType ${id}`) - } -} - /** * Validates the claims in the VC's `credentialSubject` against a CType definition. * Supports both nested and non-nested CType validation. @@ -452,13 +380,17 @@ export async function validateSubject( } }, {}) - const loaderFromCTypes: CTypeLoader | undefined = - cTypes?.length > 0 - ? ((async (id) => cTypes.find(({ $id }) => $id === id)) as CTypeLoader) - : undefined + const loaders: CTypeLoader[] = [] + if (cTypes?.length > 0) { + loaders.push((async (id) => + cTypes.find(({ $id }) => $id === id)) as CTypeLoader) + } + if (typeof loadCTypes === 'function') { + loaders.push(loadCTypes) + } // Turn CType loader & ctypes array into combined loader function - const combinedCTypeLoader = combineCTypeLoaders(loaderFromCTypes, loadCTypes) + const combinedCTypeLoader = CType.combineCTypeLoaders(...loaders) const cType = await combinedCTypeLoader(credentialsCTypeId).catch(() => { throw new Error( @@ -471,7 +403,7 @@ export async function validateSubject( } // Load all nested CTypes - const referencedCTypes = await loadNestedCTypeDefinitions( + const referencedCTypes = await CType.loadNestedCTypeDefinitions( cType, combinedCTypeLoader ) diff --git a/packages/credentials/src/ctype/CTypeLoader.ts b/packages/credentials/src/ctype/CTypeLoader.ts index 4cf5f5009..72242ffa9 100644 --- a/packages/credentials/src/ctype/CTypeLoader.ts +++ b/packages/credentials/src/ctype/CTypeLoader.ts @@ -8,6 +8,7 @@ import { ICType } from '@kiltprotocol/types' import { fetchFromChain } from './CType.chain.js' +import { idToHash, isICType } from './CType.js' export type CTypeLoader = (id: ICType['$id']) => Promise @@ -38,3 +39,74 @@ export function newCachingCTypeLoader( } return getCType } + +export async function loadNestedCTypeDefinitions( + cType: ICType, + cTypeLoader: CTypeLoader +): Promise { + const fetchedCTypeIds = new Set() + const fetchedCTypeDefinitions: ICType[] = [] + + // Don't fetch the original CType + fetchedCTypeIds.add(cType.$id) + + async function extractRefsFrom(value: unknown): Promise { + if (typeof value !== 'object' || value === null) { + return + } + + if ('$ref' in value) { + const ref = (value as { $ref: unknown }).$ref + if (typeof ref === 'string' && ref.startsWith('kilt:ctype:')) { + const cTypeId = ref.split('#/')[0] as ICType['$id'] + + if (!fetchedCTypeIds.has(cTypeId)) { + fetchedCTypeIds.add(cTypeId) + const referencedCType = await cTypeLoader(cTypeId) + + if (isICType(referencedCType)) { + fetchedCTypeDefinitions.push(referencedCType) + } else { + throw new Error(`Failed to load referenced CType: ${cTypeId}`) + } + + await extractRefsFrom(referencedCType.properties) + } + } + return + } + + // Process all values in the object. Also works for arrays + await Promise.all(Object.values(value).map(extractRefsFrom)) + } + + await extractRefsFrom(cType.properties) + + return fetchedCTypeDefinitions +} + +export function combineCTypeLoaders(...loaders: CTypeLoader[]): CTypeLoader { + const validLoaders = loaders.filter( + (l): l is CTypeLoader => typeof l === 'function' + ) + + return async (id) => { + // Ensure the ID is in the correct format + idToHash(id) + + // eslint-disable-next-line no-restricted-syntax + for (const loadCTypes of validLoaders) { + try { + // eslint-disable-next-line no-await-in-loop + const cType = await loadCTypes(id) + if (isICType(cType)) { + return cType + } + } catch { + // noop + } + } + + throw new Error(`Unable to load CType ${id}`) + } +} From 85ec86ba905fb4a3f6bffbc9576424f6ebec5c79 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 17 Feb 2025 18:37:18 +0200 Subject: [PATCH 13/15] refactor: replace combineCTypeLoaders with a caching ctype loader --- .../credentials/src/V1/KiltCredentialV1.ts | 42 ++++++--------- packages/credentials/src/ctype/CTypeLoader.ts | 53 ++++++++----------- 2 files changed, 38 insertions(+), 57 deletions(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.ts b/packages/credentials/src/V1/KiltCredentialV1.ts index 08e06ccbf..cb699c440 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.ts @@ -31,7 +31,10 @@ import { jsonLdExpandCredentialSubject, spiritnetGenesisHash, } from './common.js' -import { CTypeLoader, newCachingCTypeLoader } from '../ctype/CTypeLoader.js' +import { + type CTypeLoader, + newCachingCTypeLoader, +} from '../ctype/CTypeLoader.js' export { credentialIdFromRootHash as idFromRootHash, @@ -380,27 +383,20 @@ export async function validateSubject( } }, {}) - const loaders: CTypeLoader[] = [] - if (cTypes?.length > 0) { - loaders.push((async (id) => - cTypes.find(({ $id }) => $id === id)) as CTypeLoader) - } - if (typeof loadCTypes === 'function') { - loaders.push(loadCTypes) - } - // Turn CType loader & ctypes array into combined loader function - const combinedCTypeLoader = CType.combineCTypeLoaders(...loaders) - - const cType = await combinedCTypeLoader(credentialsCTypeId).catch(() => { - throw new Error( - `The definition for this credential's CType ${credentialsCTypeId} has not been passed to the validator and could not be loaded either` - ) - }) + const combinedCTypeLoader = newCachingCTypeLoader( + cTypes, + typeof loadCTypes === 'function' + ? loadCTypes + : (id) => + Promise.reject( + new Error( + `This credential is based on CType ${id} whose definition has not been passed to the validator, while automatic CType loading has been disabled.` + ) + ) + ) - if (cType.$id !== credentialsCTypeId) { - throw new Error('failed to load correct CType') - } + const cType = await combinedCTypeLoader(credentialsCTypeId) // Load all nested CTypes const referencedCTypes = await CType.loadNestedCTypeDefinitions( @@ -409,9 +405,5 @@ export async function validateSubject( ) // validates against CType (also validates CType schema itself) - CType.verifyClaimAgainstNestedSchemas( - cType, - Array.from(referencedCTypes), - claims - ) + CType.verifyClaimAgainstNestedSchemas(cType, referencedCTypes, claims) } diff --git a/packages/credentials/src/ctype/CTypeLoader.ts b/packages/credentials/src/ctype/CTypeLoader.ts index 72242ffa9..271c448fe 100644 --- a/packages/credentials/src/ctype/CTypeLoader.ts +++ b/packages/credentials/src/ctype/CTypeLoader.ts @@ -5,14 +5,15 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { ICType } from '@kiltprotocol/types' +import type { ICType } from '@kiltprotocol/types' +import { SDKErrors } from '@kiltprotocol/utils' import { fetchFromChain } from './CType.chain.js' -import { idToHash, isICType } from './CType.js' +import { isICType, verifyDataStructure } from './CType.js' export type CTypeLoader = (id: ICType['$id']) => Promise -const loadCType: CTypeLoader = async (id) => { +const chainCTypeLoader: CTypeLoader = async (id) => { return (await fetchFromChain(id)).cType } @@ -21,10 +22,13 @@ const loadCType: CTypeLoader = async (id) => { * Used in validating the credentialSubject of a {@link KiltCredentialV1} against the Claim Type referenced in its `type` field. * * @param initialCTypes An array of CTypes with which the cache is to be initialized. - * @returns A function that takes a CType id and looks up a CType definition in an internal cache, and if not found, tries to fetch it from the KILT blochchain. + * @param cTypeLoader A basic {@link CTypeLoader} to augment with a caching layer. + * Defaults to loading CType definitions from the KILT blockchain. + * @returns A function that takes a CType id and looks up a CType definition in an internal cache, and if not found, tries to fetch it from an external source. */ export function newCachingCTypeLoader( - initialCTypes: ICType[] = [] + initialCTypes: ICType[] = [], + cTypeLoader = chainCTypeLoader ): CTypeLoader { const ctypes: Map = new Map() @@ -33,13 +37,24 @@ export function newCachingCTypeLoader( }) async function getCType(id: ICType['$id']): Promise { - const ctype: ICType = ctypes.get(id) ?? (await loadCType(id)) + const ctype: ICType = ctypes.get(id) ?? (await cTypeLoader(id)) + verifyDataStructure(ctype) + if (id !== ctype.$id) { + throw new SDKErrors.CTypeIdMismatchError(ctype.$id, id) + } ctypes.set(ctype.$id, ctype) return ctype } return getCType } +/** + * Recursively traverses a (nested) CType's definition to load definitions of CTypes referenced within. + * + * @param cType A (nested) CType containg references to other CTypes. + * @param cTypeLoader A function with which to load CType definitions. + * @returns An array of CType definitions which were referenced in the original CType or in any of its composite CTypes. + */ export async function loadNestedCTypeDefinitions( cType: ICType, cTypeLoader: CTypeLoader @@ -84,29 +99,3 @@ export async function loadNestedCTypeDefinitions( return fetchedCTypeDefinitions } - -export function combineCTypeLoaders(...loaders: CTypeLoader[]): CTypeLoader { - const validLoaders = loaders.filter( - (l): l is CTypeLoader => typeof l === 'function' - ) - - return async (id) => { - // Ensure the ID is in the correct format - idToHash(id) - - // eslint-disable-next-line no-restricted-syntax - for (const loadCTypes of validLoaders) { - try { - // eslint-disable-next-line no-await-in-loop - const cType = await loadCTypes(id) - if (isICType(cType)) { - return cType - } - } catch { - // noop - } - } - - throw new Error(`Unable to load CType ${id}`) - } -} From 8ad84da00c1a9411c14c53a8a6dce38c480978b5 Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 17 Feb 2025 19:23:39 +0200 Subject: [PATCH 14/15] test: extend test cases --- .../src/V1/KiltCredentialV1.spec.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/credentials/src/V1/KiltCredentialV1.spec.ts b/packages/credentials/src/V1/KiltCredentialV1.spec.ts index 05fb5b71c..5f85e39f6 100644 --- a/packages/credentials/src/V1/KiltCredentialV1.spec.ts +++ b/packages/credentials/src/V1/KiltCredentialV1.spec.ts @@ -51,8 +51,25 @@ it('it verifies valid claim against nested schema', async () => { }) await expect( - validateSubject(nestedVc, { cTypes: [nestedCType, cType] }) + validateSubject(nestedVc, { + cTypes: [nestedCType, cType], + loadCTypes: false, + }) ).resolves.not.toThrow() + + await expect( + validateSubject(nestedVc, { + loadCTypes: CType.newCachingCTypeLoader([nestedCType, cType], () => + Promise.reject() + ), + }) + ).resolves.not.toThrow() + + await expect( + validateSubject(nestedVc, { cTypes: [nestedCType], loadCTypes: false }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"This credential is based on CType kilt:ctype:0xf0fd09f9ed6233b2627d37eb5d6c528345e8945e0b610e70997ed470728b2ebf whose definition has not been passed to the validator, while automatic CType loading has been disabled."` + ) }) it('it detects schema violations', async () => { From e5298ee950aececf0a6aacdd0805feb0ccf9a05c Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Mon, 17 Feb 2025 19:41:39 +0200 Subject: [PATCH 15/15] refactor: only check ctype definition on fetch --- packages/credentials/src/ctype/CTypeLoader.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/credentials/src/ctype/CTypeLoader.ts b/packages/credentials/src/ctype/CTypeLoader.ts index 9ae175876..fbc1acd1c 100644 --- a/packages/credentials/src/ctype/CTypeLoader.ts +++ b/packages/credentials/src/ctype/CTypeLoader.ts @@ -37,7 +37,11 @@ export function newCachingCTypeLoader( }) async function getCType(id: ICType['$id']): Promise { - const ctype: ICType = ctypes.get(id) ?? (await cTypeLoader(id)) + let ctype = ctypes.get(id) + if (ctype) { + return ctype + } + ctype = await cTypeLoader(id) verifyDataStructure(ctype) if (id !== ctype.$id) { throw new SDKErrors.CTypeIdMismatchError(ctype.$id, id)