diff --git a/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts b/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts new file mode 100644 index 0000000000..cee8eda160 --- /dev/null +++ b/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts @@ -0,0 +1,76 @@ +import { JsonTransformer } from '../../../utils/JsonTransformer' +import { MessageValidator } from '../../../utils/MessageValidator' +import { ProofRequest } from '../models' + +describe('ProofRequest', () => { + it('should successfully validate if the proof request json contains a valid structure', async () => { + const proofRequest = JsonTransformer.fromJSON( + { + name: 'ProofRequest', + version: '1.0', + nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + requested_attributes: { + First: { + name: 'Timo', + restrictions: [ + { + schema_id: 'string', + }, + ], + }, + }, + requested_predicates: { + Second: { + name: 'Timo', + p_type: '<=', + p_value: 10, + restrictions: [ + { + schema_id: 'string', + }, + ], + }, + }, + }, + ProofRequest + ) + + expect(MessageValidator.validate(proofRequest)).resolves.not.toThrow() + }) + + it('should throw an error if the proof request json contains an invalid structure', async () => { + const proofRequest = JsonTransformer.fromJSON( + { + name: 'ProofRequest', + version: '1.0', + nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + requested_attributes: { + First: { + names: [], + restrictions: [ + { + schema_id: 'string', + }, + ], + }, + }, + requested_predicates: [ + { + name: 'Timo', + p_type: '<=', + p_value: 10, + restrictions: [ + { + schema_id: 'string', + }, + ], + }, + ], + }, + ProofRequest + ) + + // Expect 2 top level validation errors + expect(MessageValidator.validate(proofRequest)).rejects.toHaveLength(2) + }) +}) diff --git a/packages/core/src/modules/proofs/models/ProofAttributeInfo.ts b/packages/core/src/modules/proofs/models/ProofAttributeInfo.ts index 98f81531e8..56c98888a0 100644 --- a/packages/core/src/modules/proofs/models/ProofAttributeInfo.ts +++ b/packages/core/src/modules/proofs/models/ProofAttributeInfo.ts @@ -1,5 +1,5 @@ import { Expose, Type } from 'class-transformer' -import { IsString, IsOptional, IsArray, ValidateNested, IsInstance } from 'class-validator' +import { IsString, IsOptional, IsArray, ValidateNested, IsInstance, ValidateIf, ArrayNotEmpty } from 'class-validator' import { RevocationInterval } from '../../credentials' @@ -16,12 +16,13 @@ export class ProofAttributeInfo { } @IsString() - @IsOptional() + @ValidateIf((o: ProofAttributeInfo) => o.names === undefined) public name?: string @IsArray() @IsString({ each: true }) - @IsOptional() + @ValidateIf((o: ProofAttributeInfo) => o.name === undefined) + @ArrayNotEmpty() public names?: string[] @Expose({ name: 'non_revoked' }) diff --git a/packages/core/src/modules/proofs/models/ProofPredicateInfo.ts b/packages/core/src/modules/proofs/models/ProofPredicateInfo.ts index 2fca3c162a..ba2ecdde81 100644 --- a/packages/core/src/modules/proofs/models/ProofPredicateInfo.ts +++ b/packages/core/src/modules/proofs/models/ProofPredicateInfo.ts @@ -1,5 +1,5 @@ import { Expose, Type } from 'class-transformer' -import { IsEnum, IsInstance, IsInt, IsOptional, IsString, ValidateNested } from 'class-validator' +import { IsArray, IsEnum, IsInstance, IsInt, IsOptional, IsString, ValidateNested } from 'class-validator' import { RevocationInterval } from '../../credentials' @@ -39,5 +39,6 @@ export class ProofPredicateInfo { @Type(() => AttributeFilter) @IsOptional() @IsInstance(AttributeFilter, { each: true }) + @IsArray() public restrictions?: AttributeFilter[] } diff --git a/packages/core/src/modules/proofs/models/ProofRequest.ts b/packages/core/src/modules/proofs/models/ProofRequest.ts index e24e3765f6..25f32465d5 100644 --- a/packages/core/src/modules/proofs/models/ProofRequest.ts +++ b/packages/core/src/modules/proofs/models/ProofRequest.ts @@ -4,7 +4,7 @@ import { Expose, Type } from 'class-transformer' import { IsString, ValidateNested, IsOptional, IsIn, IsInstance } from 'class-validator' import { JsonTransformer } from '../../../utils/JsonTransformer' -import { RecordTransformer } from '../../../utils/transformers' +import { IsMap } from '../../../utils/transformers' import { RevocationInterval } from '../../credentials' import { ProofAttributeInfo } from './ProofAttributeInfo' @@ -31,8 +31,12 @@ export class ProofRequest { this.name = options.name this.version = options.version this.nonce = options.nonce - this.requestedAttributes = options.requestedAttributes ?? {} - this.requestedPredicates = options.requestedPredicates ?? {} + this.requestedAttributes = options.requestedAttributes + ? new Map(Object.entries(options.requestedAttributes)) + : new Map() + this.requestedPredicates = options.requestedPredicates + ? new Map(Object.entries(options.requestedPredicates)) + : new Map() this.nonRevoked = options.nonRevoked this.ver = options.ver } @@ -48,14 +52,19 @@ export class ProofRequest { public nonce!: string @Expose({ name: 'requested_attributes' }) + @IsMap() @ValidateNested({ each: true }) - @RecordTransformer(ProofAttributeInfo) - public requestedAttributes!: Record + @Type(() => ProofAttributeInfo) + @IsInstance(ProofAttributeInfo, { each: true }) + public requestedAttributes!: Map @Expose({ name: 'requested_predicates' }) + @IsMap() @ValidateNested({ each: true }) - @RecordTransformer(ProofPredicateInfo) - public requestedPredicates!: Record + @Type(() => ProofPredicateInfo) + @IsInstance(ProofPredicateInfo, { each: true }) + public requestedPredicates!: Map + @Expose({ name: 'non_revoked' }) @ValidateNested() @Type(() => RevocationInterval) diff --git a/packages/core/src/modules/proofs/services/ProofService.ts b/packages/core/src/modules/proofs/services/ProofService.ts index 93ff63c398..8a3e1184d9 100644 --- a/packages/core/src/modules/proofs/services/ProofService.ts +++ b/packages/core/src/modules/proofs/services/ProofService.ts @@ -625,7 +625,7 @@ export class ProofService { ], }) - proofRequest.requestedAttributes[referent] = requestedAttribute + proofRequest.requestedAttributes.set(referent, requestedAttribute) } this.logger.debug('proposal predicates', presentationProposal.predicates) @@ -642,7 +642,7 @@ export class ProofService { ], }) - proofRequest.requestedPredicates[uuid()] = requestedPredicate + proofRequest.requestedPredicates.set(uuid(), requestedPredicate) } return proofRequest @@ -665,7 +665,7 @@ export class ProofService { // Get the credentialIds if it contains a hashlink for (const [referent, requestedAttribute] of Object.entries(requestedCredentials.requestedAttributes)) { // Find the requested Attributes - const requestedAttributes = indyProofRequest.requestedAttributes[referent] + const requestedAttributes = indyProofRequest.requestedAttributes.get(referent) as ProofAttributeInfo // List the requested attributes requestedAttributesNames.push(...(requestedAttributes.names ?? [requestedAttributes.name])) @@ -721,7 +721,7 @@ export class ProofService { ): Promise { const retrievedCredentials = new RetrievedCredentials({}) - for (const [referent, requestedAttribute] of Object.entries(proofRequest.requestedAttributes)) { + for (const [referent, requestedAttribute] of proofRequest.requestedAttributes.entries()) { let credentialMatch: Credential[] = [] const credentials = await this.getCredentialsForProofRequest(proofRequest, referent) @@ -759,7 +759,7 @@ export class ProofService { }) } - for (const [referent] of Object.entries(proofRequest.requestedPredicates)) { + for (const [referent] of proofRequest.requestedPredicates.entries()) { const credentials = await this.getCredentialsForProofRequest(proofRequest, referent) retrievedCredentials.requestedPredicates[referent] = credentials.map((credential) => { diff --git a/packages/core/src/utils/transformers.ts b/packages/core/src/utils/transformers.ts index 9bce0e897a..7f7350b909 100644 --- a/packages/core/src/utils/transformers.ts +++ b/packages/core/src/utils/transformers.ts @@ -1,4 +1,7 @@ +import type { ValidationOptions } from 'class-validator' + import { Transform, TransformationType } from 'class-transformer' +import { ValidateBy, buildMessage } from 'class-validator' import { DateTime } from 'luxon' import { JsonTransformer } from './JsonTransformer' @@ -55,3 +58,19 @@ export function DateParser(value: string): Date { } return new Date() } + +/** + * Checks if a given value is a Map + */ +export function IsMap(validationOptions?: ValidationOptions): PropertyDecorator { + return ValidateBy( + { + name: 'isMap', + validator: { + validate: (value: unknown): boolean => value instanceof Map, + defaultMessage: buildMessage((eachPrefix) => eachPrefix + '$property must be a Map', validationOptions), + }, + }, + validationOptions + ) +}