diff --git a/packages/authz-shared/src/lib/decorators/is-account-id.decorator.ts b/packages/authz-shared/src/lib/decorators/is-account-id.decorator.ts index af8b41867..8e81f489d 100644 --- a/packages/authz-shared/src/lib/decorators/is-account-id.decorator.ts +++ b/packages/authz-shared/src/lib/decorators/is-account-id.decorator.ts @@ -1,7 +1,7 @@ import { applyDecorators } from '@nestjs/common' -import { Matches, ValidationOptions } from 'class-validator' +import { IsDefined, Matches, ValidationOptions } from 'class-validator' export function IsAccountId(validationOptions?: ValidationOptions) { const regex = new RegExp('^eip155:d+/w+$') - return applyDecorators(Matches(regex, validationOptions)) + return applyDecorators(IsDefined(), Matches(regex, validationOptions)) } diff --git a/packages/authz-shared/src/lib/decorators/is-asset-id.decorator.ts b/packages/authz-shared/src/lib/decorators/is-asset-id.decorator.ts index aed1e8d05..9ea5ef41c 100644 --- a/packages/authz-shared/src/lib/decorators/is-asset-id.decorator.ts +++ b/packages/authz-shared/src/lib/decorators/is-asset-id.decorator.ts @@ -1,7 +1,7 @@ import { applyDecorators } from '@nestjs/common' -import { Matches, ValidationOptions } from 'class-validator' +import { IsDefined, Matches, ValidationOptions } from 'class-validator' export function IsAssetId(validationOptions?: ValidationOptions) { const regex = new RegExp('^(eip155:d+/(erc1155|erc20|erc721):w+/w+|eip155:d+/slip44:d+)$') - return applyDecorators(Matches(regex, validationOptions)) + return applyDecorators(IsDefined(), Matches(regex, validationOptions)) } diff --git a/packages/authz-shared/src/lib/decorators/is-hex-string.decorator.ts b/packages/authz-shared/src/lib/decorators/is-hex-string.decorator.ts new file mode 100644 index 000000000..1059fdd59 --- /dev/null +++ b/packages/authz-shared/src/lib/decorators/is-hex-string.decorator.ts @@ -0,0 +1,7 @@ +import { applyDecorators } from '@nestjs/common' +import { IsDefined, Matches, ValidationOptions } from 'class-validator' + +export function IsHexString(validationOptions?: ValidationOptions) { + const regex = new RegExp('^0x[a-fA-F0-9]+$') + return applyDecorators(IsDefined(), Matches(regex, validationOptions)) +} diff --git a/packages/authz-shared/src/lib/decorators/is-not-empty-array-enum.decorator.ts b/packages/authz-shared/src/lib/decorators/is-not-empty-array-enum.decorator.ts index bd0be20a8..72225860c 100644 --- a/packages/authz-shared/src/lib/decorators/is-not-empty-array-enum.decorator.ts +++ b/packages/authz-shared/src/lib/decorators/is-not-empty-array-enum.decorator.ts @@ -4,13 +4,10 @@ import { ArrayMinSize, IsArray, IsDefined, IsEnum } from 'class-validator' export function IsNotEmptyArrayEnum(Enum: object) { return applyDecorators( - IsDefined, - IsArray, + IsDefined(), + IsArray(), IsEnum(Enum, { each: true }), - ApiProperty({ - enum: Object.values(Enum), - isArray: true - }), - ArrayMinSize(1) + ArrayMinSize(1), + ApiProperty({ enum: Object.values(Enum), isArray: true }) ) } diff --git a/packages/authz-shared/src/lib/decorators/is-not-empty-array-string.decorator.ts b/packages/authz-shared/src/lib/decorators/is-not-empty-array-string.decorator.ts index b8ef2d595..a7bbb50c2 100644 --- a/packages/authz-shared/src/lib/decorators/is-not-empty-array-string.decorator.ts +++ b/packages/authz-shared/src/lib/decorators/is-not-empty-array-string.decorator.ts @@ -1,13 +1,13 @@ -import { applyDecorators } from '@nestjs/common'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsDefined, IsArray, IsString, ArrayMinSize } from 'class-validator'; +import { applyDecorators } from '@nestjs/common' +import { ApiProperty } from '@nestjs/swagger' +import { ArrayMinSize, IsArray, IsDefined, IsString } from 'class-validator' export function IsNotEmptyArrayString() { return applyDecorators( IsDefined(), IsArray(), IsString({ each: true }), - ApiProperty({ type: String, isArray: true }), - ArrayMinSize(1) - ); -} \ No newline at end of file + ArrayMinSize(1), + ApiProperty({ type: String, isArray: true }) + ) +} diff --git a/packages/authz-shared/src/lib/type/policy-builder.type.ts b/packages/authz-shared/src/lib/type/policy-builder.type.ts index fe1a6699e..9441f5d21 100644 --- a/packages/authz-shared/src/lib/type/policy-builder.type.ts +++ b/packages/authz-shared/src/lib/type/policy-builder.type.ts @@ -14,10 +14,21 @@ import { } from '@narval/authz-shared' import { Intents } from '@narval/transaction-request-intent' import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger' -import { Transform, plainToInstance } from 'class-transformer' -import { IsDefined, IsIn, IsString, Matches, ValidateNested } from 'class-validator' +import { Transform, Type, plainToInstance } from 'class-transformer' +import { + IsBoolean, + IsDefined, + IsIn, + IsNotEmpty, + IsNumber, + IsNumberString, + IsOptional, + IsString, + ValidateNested +} from 'class-validator' import { IsAccountId } from '../decorators/is-account-id.decorator' import { IsAssetId } from '../decorators/is-asset-id.decorator' +import { IsHexString } from '../decorators/is-hex-string.decorator' import { IsNotEmptyArrayEnum } from '../decorators/is-not-empty-array-enum.decorator' import { IsNotEmptyArrayString } from '../decorators/is-not-empty-array-string.decorator' import { ValidateCriterion } from '../decorators/validate-criterion.decorator' @@ -68,59 +79,150 @@ export const Criterion = { export type Criterion = (typeof Criterion)[keyof typeof Criterion] -export type AmountCondition = { - currency: `${FiatCurrency}` | '*' - operator: `${ValueOperators}` +export class AmountCondition { + @IsIn([...Object.values(FiatCurrency), '*']) + currency: FiatCurrency | '*' + + @IsNotEmptyArrayEnum(ValueOperators) + operator: ValueOperators + + @IsNotEmpty() + @IsNumberString() value: string } -export type ERC1155AmountCondition = { +export class ERC1155AmountCondition { + @IsAssetId() tokenId: AssetId - operator: `${ValueOperators}` + + @IsNotEmptyArrayEnum(ValueOperators) + operator: ValueOperators + + @IsNotEmpty() + @IsNumberString() value: string } -export type SignMessageCondition = { - operator: `${ValueOperators.EQUAL}` | `${typeof IdentityOperators.CONTAINS}` +export class SignMessageCondition { + @IsIn([ValueOperators.EQUAL, IdentityOperators.CONTAINS]) + operator: ValueOperators.EQUAL | IdentityOperators.CONTAINS + + @IsNotEmpty() + @IsString() value: string } -export type SignTypedDataDomainCondition = { +export class SignTypedDataDomainCondition { + @IsOptional() + @IsNotEmptyArrayString() + @IsNumberString({}, { each: true }) version?: string[] + + @IsOptional() + @IsNotEmptyArrayString() + @IsNumberString({}, { each: true }) chainId?: string[] + + @IsOptional() + @IsNotEmptyArrayString() name?: string[] + + @IsOptional() + @IsNotEmptyArrayString() + @IsHexString({ each: true }) verifyingContract?: Address[] } -export type PermitDeadlineCondition = { - operator: `${ValueOperators}` +export class PermitDeadlineCondition { + @IsNotEmptyArrayEnum(ValueOperators) + operator: ValueOperators + + @IsNotEmpty() + @IsNumberString() value: string // timestamp in ms } -export type ApprovalCondition = { +export class ApprovalCondition { + @IsDefined() + @IsNumber() approvalCount: number + + @IsDefined() + @IsBoolean() countPrincipal: boolean - approvalEntityType: `${EntityType}` + + @IsDefined() + @IsIn(Object.values(EntityType)) + approvalEntityType: EntityType + + @IsNotEmptyArrayString() entityIds: string[] } -export type SpendingLimitCondition = { +export class SpendingLimitTimeWindow { + @IsIn(['rolling', 'fixed']) + type?: 'rolling' | 'fixed' + + @IsNumber() + @IsOptional() + value?: number // in seconds + + @IsNumber() + @IsOptional() + startDate?: number // in seconds + + @IsNumber() + @IsOptional() + endDate?: number // in seconds +} + +export class SpendingLimitFilters { + @IsNotEmptyArrayString() + @IsAccountId({ each: true }) + @IsOptional() + tokens?: AccountId[] + + @IsNotEmptyArrayString() + @IsOptional() + users?: string[] + + @IsNotEmptyArrayString() + @IsAccountId({ each: true }) + @IsOptional() + resources?: AccountId[] + + @IsNotEmptyArrayString() + @IsNumberString({}, { each: true }) + @IsOptional() + chains?: string[] + + @IsNotEmptyArrayString() + @IsOptional() + userGroups?: string[] + + @IsNotEmptyArrayString() + @IsOptional() + walletGroups?: string[] +} + +export class SpendingLimitCondition { + @IsString() + @IsNotEmpty() limit: string - currency?: `${FiatCurrency}` - timeWindow?: { - type?: 'rolling' | 'fixed' - value?: number // in seconds - startDate?: number // in seconds - endDate?: number // in seconds - } - filters?: { - tokens?: AccountId[] - users?: string[] - resources?: AccountId[] - chains?: string[] - userGroups?: string[] - walletGroups?: string[] - } + + @IsIn(Object.values(FiatCurrency)) + @IsOptional() + currency?: FiatCurrency + + @ValidateNested() + @Type(() => SpendingLimitTimeWindow) + @IsOptional() + timeWindow?: SpendingLimitTimeWindow + + @ValidateNested() + @Type(() => SpendingLimitFilters) + @IsOptional() + filters?: SpendingLimitFilters } class BaseCriterion { @@ -287,7 +389,7 @@ class IntentHexSignatureCriterion extends BaseCriterion { criterion: typeof Criterion.CHECK_INTENT_HEX_SIGNATURE @IsNotEmptyArrayString() - @Matches(/^0x[a-fA-F0-9]+$/, { each: true }) + @IsHexString({ each: true }) args: Hex[] } @@ -295,6 +397,8 @@ class IntentAmountCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_AMOUNT) criterion: typeof Criterion.CHECK_INTENT_AMOUNT + @ValidateNested() + @Type(() => AmountCondition) args: AmountCondition } @@ -320,6 +424,8 @@ class ERC1155TransfersCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_ERC1155_TRANSFERS) criterion: typeof Criterion.CHECK_ERC1155_TRANSFERS + @ValidateNested() + @Type(() => ERC1155AmountCondition) args: ERC1155AmountCondition[] } @@ -327,6 +433,8 @@ class IntentMessageCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_MESSAGE) criterion: typeof Criterion.CHECK_INTENT_MESSAGE + @ValidateNested() + @Type(() => SignMessageCondition) args: SignMessageCondition } @@ -350,6 +458,8 @@ class IntentDomainCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_INTENT_DOMAIN) criterion: typeof Criterion.CHECK_INTENT_DOMAIN + @ValidateNested() + @Type(() => SignTypedDataDomainCondition) args: SignTypedDataDomainCondition } @@ -357,6 +467,8 @@ class PermitDeadlineCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_PERMIT_DEADLINE) criterion: typeof Criterion.CHECK_PERMIT_DEADLINE + @ValidateNested() + @Type(() => PermitDeadlineCondition) args: PermitDeadlineCondition } @@ -364,6 +476,8 @@ class GasFeeAmountCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_GAS_FEE_AMOUNT) criterion: typeof Criterion.CHECK_GAS_FEE_AMOUNT + @ValidateNested() + @Type(() => AmountCondition) args: AmountCondition } @@ -385,6 +499,8 @@ class ApprovalsCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_APPROVALS) criterion: typeof Criterion.CHECK_APPROVALS + @ValidateNested() + @Type(() => ApprovalCondition) args: ApprovalCondition[] } @@ -392,6 +508,8 @@ class SpendingLimitCriterion extends BaseCriterion { @ValidateCriterion(Criterion.CHECK_SPENDING_LIMIT) criterion: typeof Criterion.CHECK_SPENDING_LIMIT + @ValidateNested() + @Type(() => SpendingLimitCondition) args: SpendingLimitCondition }