From db4b2d5c6c15ce308dd12931c0129887143ad462 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 25 Oct 2024 16:42:18 +0200 Subject: [PATCH 1/2] chore: improve error handling definitionVersionDiscovery --- jest.config.js | 2 +- lib/PEX.ts | 17 +++++++- lib/types/Internal.types.ts | 6 ++- lib/utils/VCUtils.ts | 38 +++++++++-------- lib/validation/validatePDv1.d.ts | 6 +++ lib/validation/validatePDv2.d.ts | 6 +++ lib/validation/validators.d.ts | 71 ++++++++++++++++++++++++++++++++ test/PEXv2.spec.ts | 17 ++++++++ tsconfig.json | 6 ++- 9 files changed, 145 insertions(+), 24 deletions(-) create mode 100644 lib/validation/validatePDv1.d.ts create mode 100644 lib/validation/validatePDv2.d.ts create mode 100644 lib/validation/validators.d.ts diff --git a/jest.config.js b/jest.config.js index d0aeb38a..b04f73be 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,5 +2,5 @@ module.exports = { transform: {'^.+\\.ts?$': 'ts-jest'}, testEnvironment: 'node', testRegex: '/test/.*\\.(test|spec)?\\.ts$', - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node', 'd.ts'] }; diff --git a/lib/PEX.ts b/lib/PEX.ts index 37cce6d4..d159dba5 100644 --- a/lib/PEX.ts +++ b/lib/PEX.ts @@ -30,7 +30,7 @@ import { VerifiablePresentationResult, } from './signing'; import { DiscoveredVersion, IInternalPresentationDefinition, IPresentationDefinition, OrArray, PEVersion, SSITypesBuilder } from './types'; -import { calculateSdHash, definitionVersionDiscovery, getSubjectIdsAsString } from './utils'; +import { calculateSdHash, definitionVersionDiscovery, formatValidationErrors, getSubjectIdsAsString } from './utils'; import { PresentationDefinitionV1VB, PresentationDefinitionV2VB, PresentationSubmissionVB, Validated, ValidationEngine } from './validation'; export interface PEXOptions { @@ -444,8 +444,21 @@ export class PEX { public static validateDefinition(presentationDefinition: IPresentationDefinition): Validated { const result = definitionVersionDiscovery(presentationDefinition); if (result.error) { - throw new Error(result.error); + const errorParts = [result.error]; + + const v1ErrorString = formatValidationErrors(result.v1Errors); + if (v1ErrorString) { + errorParts.push('\nVersion 1 validation errors:\n ' + v1ErrorString); + } + + const v2ErrorString = formatValidationErrors(result.v2Errors); + if (v2ErrorString) { + errorParts.push('\nVersion 2 validation errors:\n ' + v2ErrorString); + } + + throw new Error(errorParts.join('')); } + const validators = []; result.version === PEVersion.v1 ? validators.push({ diff --git a/lib/types/Internal.types.ts b/lib/types/Internal.types.ts index 6f3b03e7..9ecb2256 100644 --- a/lib/types/Internal.types.ts +++ b/lib/types/Internal.types.ts @@ -8,6 +8,8 @@ import { } from '@sphereon/pex-models'; import { IVerifiableCredential, IVerifiablePresentation } from '@sphereon/ssi-types'; +import { ValidationError } from '../validation/validators'; + export interface InputDescriptorWithIndex { inputDescriptorIndex: number; inputDescriptor: InputDescriptorV1 | InputDescriptorV2; @@ -92,8 +94,8 @@ export class InternalPresentationDefinitionV2 implements PresentationDefinitionV export interface DiscoveredVersion { version?: PEVersion; error?: string; - v1Errors?: Record; - v2Errors?: Record; + v1Errors?: Array; + v2Errors?: Array; } export type IPresentationDefinition = PresentationDefinitionV1 | PresentationDefinitionV2; diff --git a/lib/utils/VCUtils.ts b/lib/utils/VCUtils.ts index 82d4115b..3150d21f 100644 --- a/lib/utils/VCUtils.ts +++ b/lib/utils/VCUtils.ts @@ -1,12 +1,9 @@ import { AdditionalClaims, CredentialMapper, ICredential, ICredentialSubject, IIssuer, SdJwtDecodedVerifiableCredential } from '@sphereon/ssi-types'; import { DiscoveredVersion, IPresentationDefinition, PEVersion } from '../types'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore import validatePDv1 from '../validation/validatePDv1.js'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore import validatePDv2 from '../validation/validatePDv2.js'; +import { ValidationError } from '../validation/validators'; import { ObjectUtils } from './ObjectUtils'; import { JsonPathUtils } from './jsonPathUtils'; @@ -34,27 +31,32 @@ export function definitionVersionDiscovery(presentationDefinition: IPresentation JsonPathUtils.changePropertyNameRecursively(presentationDefinitionCopy, '_const', 'const'); JsonPathUtils.changePropertyNameRecursively(presentationDefinitionCopy, '_enum', 'enum'); const data = { presentation_definition: presentationDefinitionCopy }; - let result = validatePDv2(data); - - if (result) { + if (validatePDv2(data)) { return { version: PEVersion.v2 }; } - // Errors are added to the validation method, but not typed correctly - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const v2Errors = validatePDv2.errors; + const v2Errors = validatePDv2.errors ?? undefined; - result = validatePDv1(data); - if (result) { + if (validatePDv1(data)) { return { version: PEVersion.v1 }; } + const v1Errors = validatePDv1.errors ?? undefined; - // Errors are added to the validation method, but not typed correctly - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const v1Errors = validatePDv1.errors; + return { + error: 'This is not a valid PresentationDefinition', + v1Errors, + v2Errors, + }; +} - return { error: 'This is not a valid PresentationDefinition', v1Errors, v2Errors }; +export function formatValidationError(error: ValidationError): string { + return `${error.instancePath || '/'}: ${error.message}${error.params.additionalProperty ? ` (${error.params.additionalProperty})` : ''}`; +} + +export function formatValidationErrors(errors: ValidationError[] | undefined): string | undefined { + if (!errors?.length) { + return undefined; + } + return errors.map(formatValidationError).join('\n '); } export function uniformDIDMethods(dids?: string[], opts?: { removePrefix: 'did:' }) { diff --git a/lib/validation/validatePDv1.d.ts b/lib/validation/validatePDv1.d.ts new file mode 100644 index 00000000..5a036a84 --- /dev/null +++ b/lib/validation/validatePDv1.d.ts @@ -0,0 +1,6 @@ +import type { ValidateFunction, ValidationError } from './validators'; + +declare module './validatePDv1.js' { + const validate: ValidateFunction & { errors?: ValidationError[] | null }; + export default validate; +} diff --git a/lib/validation/validatePDv2.d.ts b/lib/validation/validatePDv2.d.ts new file mode 100644 index 00000000..3de5658d --- /dev/null +++ b/lib/validation/validatePDv2.d.ts @@ -0,0 +1,6 @@ +import type { ValidateFunction, ValidationError } from './validators'; + +declare module './validatePDv2.js' { + const validate: ValidateFunction & { errors?: ValidationError[] | null }; + export default validate; +} diff --git a/lib/validation/validators.d.ts b/lib/validation/validators.d.ts new file mode 100644 index 00000000..35b41480 --- /dev/null +++ b/lib/validation/validators.d.ts @@ -0,0 +1,71 @@ +export interface ValidateFunction { + (data: unknown): boolean; + errors?: ValidationError[]; +} + +export type ValidationParentSchema = { + type?: string | string[]; + properties?: { + [key: string]: { + type?: string; + enum?: string[]; + items?: { + type?: string; + $ref?: string; + }; + properties?: Record; + required?: string[]; + additionalProperties?: boolean; + $ref?: string; + }; + }; + required?: string[]; + additionalProperties?: boolean; + items?: { + type?: string; + $ref?: string; + }; +}; + +export type ValidationData = { + id?: string; + name?: string; + purpose?: string; + constraints?: { + limit_disclosure?: string; + fields?: Array<{ + path?: string[]; + purpose?: string; + filter?: { + type?: string; + pattern?: string; + }; + }>; + }; + schema?: Array<{ + uri?: string; + }>; + [key: string]: unknown; +}; + +export type ValidationError = { + instancePath: string; + schemaPath: string; + keyword: string; + params: { + type?: string | string[]; + limit?: number; + comparison?: string; + missingProperty?: string; + additionalProperty?: string; + propertyName?: string; + i?: number; + j?: number; + allowedValues?: string[] | readonly string[]; + passingSchemas?: number | number[]; + }; + message: string; + schema: boolean | ValidationParentSchema; + parentSchema: ValidationParentSchema; + data: ValidationData; +}; diff --git a/test/PEXv2.spec.ts b/test/PEXv2.spec.ts index edf31ef0..7eabb257 100644 --- a/test/PEXv2.spec.ts +++ b/test/PEXv2.spec.ts @@ -325,6 +325,23 @@ describe('evaluate', () => { expect(result!.areRequiredCredentialsPresent).toBe('info'); }); + it('Evaluate selectFrom should fail', () => { + const pd: PresentationDefinitionV2 = getPresentationDefinitionV2_1(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (pd as any).input_descriptors = undefined; + const result1 = PEX.definitionVersionDiscovery(pd); + expect(result1.v1Errors?.length).toBe(2); + expect(result1.v2Errors?.length).toBe(1); + expect(() => PEX.validateDefinition(pd)).toThrow( + 'This is not a valid PresentationDefinition\n' + + 'Version 1 validation errors:\n' + + " /presentation_definition: must have required property 'input_descriptors'\n" + + ' /presentation_definition: must NOT have additional properties (frame)\n' + + 'Version 2 validation errors:\n' + + " /presentation_definition: must have required property 'input_descriptors'", + ); + }); + it("should throw error if proofOptions doesn't have a type with v2 pd", async () => { const pdSchema = getFile('./test/dif_pe_examples/pdV2/vc_expiration(corrected).json'); const vpSimple = getFile('./test/dif_pe_examples/vp/vp_general.json') as IVerifiablePresentation; diff --git a/tsconfig.json b/tsconfig.json index 48689696..8a4055f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,7 +38,11 @@ "lib": [ "es7", "dom" ],*/ - "types": ["jest", "node"] + "types": ["jest", "node"], + "typeRoots": [ + "./node_modules/@types", + "./src/validation" + ] }, "include": [ "declarations.d.ts", From c235d89375fa60802165efe0e86bccdee1fa9401 Mon Sep 17 00:00:00 2001 From: sanderPostma Date: Fri, 25 Oct 2024 16:56:03 +0200 Subject: [PATCH 2/2] chore: simplify types --- lib/validation/validators.d.ts | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/lib/validation/validators.d.ts b/lib/validation/validators.d.ts index 35b41480..d975ee82 100644 --- a/lib/validation/validators.d.ts +++ b/lib/validation/validators.d.ts @@ -1,5 +1,8 @@ +import { PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models'; + export interface ValidateFunction { (data: unknown): boolean; + errors?: ValidationError[]; } @@ -27,27 +30,6 @@ export type ValidationParentSchema = { }; }; -export type ValidationData = { - id?: string; - name?: string; - purpose?: string; - constraints?: { - limit_disclosure?: string; - fields?: Array<{ - path?: string[]; - purpose?: string; - filter?: { - type?: string; - pattern?: string; - }; - }>; - }; - schema?: Array<{ - uri?: string; - }>; - [key: string]: unknown; -}; - export type ValidationError = { instancePath: string; schemaPath: string; @@ -67,5 +49,5 @@ export type ValidationError = { message: string; schema: boolean | ValidationParentSchema; parentSchema: ValidationParentSchema; - data: ValidationData; + data: PresentationDefinitionV1 | PresentationDefinitionV2; };