diff --git a/.github/workflows/authz_ci.yml b/.github/workflows/authz_ci.yml index 8ebd22303..9422766f6 100644 --- a/.github/workflows/authz_ci.yml +++ b/.github/workflows/authz_ci.yml @@ -103,3 +103,20 @@ jobs: fields: message,commit,author env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + opa-rego: + name: Open Agent Policy CI + + runs-on: ubuntu-latest + + steps: + - name: Check out repository code + uses: actions/checkout@v3 + + - name: Setup OPA + uses: open-policy-agent/setup-opa@v2 + with: + version: latest + + - name: Run OPA Tests + run: make authz/rego/test \ No newline at end of file diff --git a/.github/workflows/authz_opa_ci.yml b/.github/workflows/authz_opa_ci.yml new file mode 100644 index 000000000..9b7db7cd2 --- /dev/null +++ b/.github/workflows/authz_opa_ci.yml @@ -0,0 +1,25 @@ +name: '@app/authz OPA CI' + +on: + push: + paths: + - apps/authz/src/opa/rego/** + - .github/workflows/authz_opa_ci.yml + +jobs: + opa-rego: + name: Open Agent Policy CI + + runs-on: ubuntu-latest + + steps: + - name: Check out repository code + uses: actions/checkout@v3 + + - name: Setup OPA + uses: open-policy-agent/setup-opa@v2 + with: + version: latest + + - name: Run OPA Tests + run: make authz/rego/test \ No newline at end of file diff --git a/.github/workflows/transaction_request_intent_ci.yml b/.github/workflows/transaction_request_intent_ci.yml index c4cbf2f5d..8378bea39 100644 --- a/.github/workflows/transaction_request_intent_ci.yml +++ b/.github/workflows/transaction_request_intent_ci.yml @@ -42,6 +42,11 @@ jobs: run: | make transaction-request-intent/test/type + - name: Test upstream application types + shell: bash + run: | + make authz/test/type + - name: Test unit shell: bash run: | diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 000000000..31354ec13 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..36af21989 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/apps/authz/.lintstagedrc.js b/apps/authz/.lintstagedrc.js new file mode 100644 index 000000000..3cc2540b7 --- /dev/null +++ b/apps/authz/.lintstagedrc.js @@ -0,0 +1,6 @@ +module.exports = { + '*.{ts,tsx}': (filenames) => [ + `eslint --no-error-on-unmatched-pattern ${filenames.join(' ')}; echo "ESLint completed with exit code $?"`, + `prettier --write ${filenames.join(' ')}` + ] +} diff --git a/apps/authz/Makefile b/apps/authz/Makefile index 62a3cde5d..d03662e8b 100644 --- a/apps/authz/Makefile +++ b/apps/authz/Makefile @@ -53,7 +53,7 @@ authz/rego/compile: opa build \ --target wasm \ --entrypoint main/evaluate \ - --bundle ${AUTHZ_PROJECT_DIR}/src/app/opa/rego \ + --bundle ${AUTHZ_PROJECT_DIR}/src/opa/rego \ --ignore "__test__" \ --output ./rego-build/policies.gz tar -xzf ./rego-build/policies.gz -C ./rego-build/ @@ -61,28 +61,31 @@ authz/rego/compile: authz/rego/wasm: npx ts-node \ --compiler-options "{\"module\":\"CommonJS\"}" \ - ${AUTHZ_PROJECT_DIR}/src/app/opa/rego/script.ts + ${AUTHZ_PROJECT_DIR}/src/opa/rego/script.ts authz/rego/bundle: - rm -rf ${AUTHZ_PROJECT_DIR}/src/app/opa/build + rm -rf ${AUTHZ_PROJECT_DIR}/src/opa/build - mkdir -p ${AUTHZ_PROJECT_DIR}/src/app/opa/build + mkdir -p ${AUTHZ_PROJECT_DIR}/src/opa/build opa build \ - --bundle ${AUTHZ_PROJECT_DIR}/src/app/opa/rego \ + --bundle ${AUTHZ_PROJECT_DIR}/src/opa/rego \ --ignore "__test__" \ - --output ${AUTHZ_PROJECT_DIR}/src/app/opa/build/policies.tar.gz + --output ${AUTHZ_PROJECT_DIR}/src/opa/build/policies.tar.gz authz/rego/eval: opa eval \ --format="pretty" \ - --bundle ${AUTHZ_PROJECT_DIR}/src/app/opa/build/policies.tar.gz \ - --input ${AUTHZ_PROJECT_DIR}/src/app/opa/rego/input.json \ + --bundle ${AUTHZ_PROJECT_DIR}/src/opa/build/policies.tar.gz \ + --input ${AUTHZ_PROJECT_DIR}/src/opa/rego/input.json \ 'data.main.evaluate' authz/rego/test: opa test \ --format="pretty" \ - ${AUTHZ_PROJECT_DIR}/src/app/opa/rego \ + ${AUTHZ_PROJECT_DIR}/src/opa/rego \ --verbose \ - --watch + ${ARGS} + +authz/rego/test/watch: + make authz/rego/test ARGS=--watch \ No newline at end of file diff --git a/apps/authz/src/app/app.controller.ts b/apps/authz/src/app/app.controller.ts index 774f63445..77cb7bcd9 100644 --- a/apps/authz/src/app/app.controller.ts +++ b/apps/authz/src/app/app.controller.ts @@ -1,6 +1,6 @@ import { EvaluationRequestDto } from '@app/authz/app/evaluation-request.dto' import { generateInboundRequest } from '@app/authz/shared/module/persistence/mock_data' -import { AuthZRequestPayload } from '@app/authz/shared/types/domain.type' +import { AuthorizationRequestPayload } from '@app/authz/shared/types/domain.type' import { Body, Controller, Get, Logger, Post } from '@nestjs/common' import { AppService } from './app.service' @@ -27,11 +27,12 @@ export class AppController { @Post('/evaluation') async evaluate(@Body() body: EvaluationRequestDto) { this.logger.log({ - message: 'Received evaluation' + message: 'Received evaluation', + body }) // Map the DTO into the TS type because it's nicer to deal with. - const payload: AuthZRequestPayload = body + const payload: AuthorizationRequestPayload = body const result = await this.appService.runEvaluation(payload) this.logger.log({ @@ -44,10 +45,11 @@ export class AppController { @Post('/evaluation-demo') async evaluateDemo() { + const fakeRequest = await generateInboundRequest() this.logger.log({ - message: 'Received evaluation' + message: 'Received evaluation', + body: fakeRequest }) - const fakeRequest = await generateInboundRequest() const result = await this.appService.runEvaluation(fakeRequest) this.logger.log({ message: 'Evaluation Result', @@ -59,4 +61,9 @@ export class AppController { result } } + + @Get('/generate-inbound-request') + generateInboundRequest() { + return generateInboundRequest() + } } diff --git a/apps/authz/src/app/app.service.ts b/apps/authz/src/app/app.service.ts index 5284fea9b..b994ac3c4 100644 --- a/apps/authz/src/app/app.service.ts +++ b/apps/authz/src/app/app.service.ts @@ -2,15 +2,15 @@ import { PersistenceRepository } from '@app/authz/shared/module/persistence/pers import { Alg, AuthCredential, - AuthZRequest, - AuthZRequestPayload, - AuthZResponse, + AuthorizationRequest, + AuthorizationRequestPayload, + AuthorizationResponse, HistoricalTransfer, NarvalDecision, RequestSignature } from '@app/authz/shared/types/domain.type' import { OpaResult, RegoInput } from '@app/authz/shared/types/rego' -import { hashRequest } from '@narval/authz-shared' +import { Action, hashRequest } from '@narval/authz-shared' import { Injectable } from '@nestjs/common' import { Decoder } from 'packages/transaction-request-intent/src' import { InputType } from 'packages/transaction-request-intent/src/lib/domain' @@ -75,7 +75,14 @@ export class AppService { address, signature: signature as Hex }) - if (!valid) throw new Error('Invalid Signature') + if (!valid) { + console.log('### invalid', { + pubKey, + sig + }) + + throw new Error('Invalid Signature') + } } // TODO: verify other alg types @@ -104,47 +111,56 @@ export class AppService { transfers }: { principal: AuthCredential - request: AuthZRequest + request: AuthorizationRequest approvals: AuthCredential[] | null intent?: Intent transfers?: HistoricalTransfer[] }): RegoInput { - // intent only exists in SignTransaction actions - return { - action: request.action, - intent, - transactionRequest: request.transactionRequest, - principal, - resource: request.resourceId - ? { - uid: request.resourceId - } - : undefined, - approvals: approvals || [], - transfers: transfers || [] + if (request.action === Action.SIGN_TRANSACTION) { + return { + action: Action.SIGN_TRANSACTION, + intent, + transactionRequest: request.transactionRequest, + principal, + resource: request.resourceId + ? { + uid: request.resourceId + } + : undefined, + approvals: approvals || [], + transfers: transfers || [] + } } + + throw new Error(`Unsupported action ${request.action}`) } /** * Actual Eval Flow */ - async runEvaluation({ request, authentication, approvals, transfers }: AuthZRequestPayload) { + async runEvaluation({ request, authentication, approvals, transfers }: AuthorizationRequestPayload) { // Pre-Process // verify the signatures of the Principal and any Approvals - const decoder = new Decoder() + const decoder = new Decoder({}) const verificationMessage = hashRequest(request) + const principalCredential = await this.#verifySignature(authentication, verificationMessage) if (!principalCredential) throw new Error(`Could not find principal`) const populatedApprovals = await this.#populateApprovals(approvals, verificationMessage) // Decode the intent - const intentResult = request.transactionRequest - ? decoder.safeDecode({ - type: InputType.TRANSACTION_REQUEST, - txRequest: request.transactionRequest - }) - : undefined - if (intentResult?.success === false) throw new Error(`Could not decode intent: ${intentResult.error.message}`) + const intentResult = + request.action === Action.SIGN_TRANSACTION + ? decoder.safeDecode({ + type: InputType.TRANSACTION_REQUEST, + txRequest: request.transactionRequest + }) + : undefined + + if (intentResult?.success === false) { + throw new Error(`Could not decode intent: ${intentResult.error.message}`) + } + const intent = intentResult?.intent const input = this.#buildRegoInput({ @@ -163,7 +179,7 @@ export class AppService { // Post-processing to evaluate multisigs const finalDecision = finalizeDecision(resultSet) - const authzResponse: AuthZResponse = { + const authzResponse: AuthorizationResponse = { decision: finalDecision.decision, request, totalApprovalsRequired: finalDecision.totalApprovalsRequired, diff --git a/apps/authz/src/app/evaluation-request.dto.ts b/apps/authz/src/app/evaluation-request.dto.ts index 547c1d448..bb38a06c5 100644 --- a/apps/authz/src/app/evaluation-request.dto.ts +++ b/apps/authz/src/app/evaluation-request.dto.ts @@ -96,13 +96,20 @@ export class TransactionRequestDto { } export class SignTransactionRequestDataDto extends BaseRequestDataDto { + @IsEnum(Action) + @IsDefined() + @ApiProperty({ + enum: Action, + default: Action.SIGN_TRANSACTION + }) + action: Action.SIGN_TRANSACTION + @IsString() @IsDefined() @ApiProperty() resourceId: string @ValidateNested() - @Type(() => TransactionRequestDto) @IsDefined() @ApiProperty({ type: TransactionRequestDto @@ -111,6 +118,14 @@ export class SignTransactionRequestDataDto extends BaseRequestDataDto { } export class SignMessageRequestDataDto extends BaseRequestDataDto { + @IsEnum(Action) + @IsDefined() + @ApiProperty({ + enum: Action, + default: Action.SIGN_MESSAGE + }) + action: Action.SIGN_MESSAGE + @IsString() @IsDefined() @ApiProperty() @@ -139,6 +154,8 @@ export class EvaluationRequestDto { @ApiProperty() authentication: RequestSignatureDto + @IsOptional() + @ValidateNested() @ApiProperty({ type: () => RequestSignatureDto, isArray: true @@ -157,19 +174,8 @@ export class EvaluationRequestDto { }) request: SignTransactionRequestDataDto | SignMessageRequestDataDto + @IsOptional() @ValidateNested() @ApiProperty() transfers?: HistoricalTransferDto[] - - isSignTransaction( - request: SignTransactionRequestDataDto | SignMessageRequestDataDto - ): request is SignTransactionRequestDataDto { - return this.request.action === Action.SIGN_TRANSACTION - } - - isSignMessage( - request: SignTransactionRequestDataDto | SignMessageRequestDataDto - ): request is SignMessageRequestDataDto { - return this.request.action === Action.SIGN_MESSAGE - } } diff --git a/apps/authz/src/app/opa/rego/input.json b/apps/authz/src/app/opa/rego/input.json deleted file mode 100644 index 3ccab38a3..000000000 --- a/apps/authz/src/app/opa/rego/input.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "action": "signTransaction", - "intent": { - "to": "eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4", - "from": "eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b", - "type": "transferNative", - "amount": "1000000000000000000", - "token": "eip155:137/slip44/966" - }, - "transactionRequest": { - "from": "0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b", - "to": "0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4", - "chainId": 137, - "value": "0xde0b6b3a7640000", - "data": "0x00000000", - "nonce": 192, - "type": "2" - }, - "principal": { - "id": "credentialId1", - "alg": "ES256K", - "userId": "matt@narval.xyz", - "pubKey": "0xd75D626a116D4a1959fE3bB938B2e7c116A05890" - }, - "resource": { "uid": "eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b" }, - "approvals": [ - { - "userId": "aa@narval.xyz", - "id": "credentialId2", - "alg": "ES256K", - "pubKey": "0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06" - }, - { - "userId": "bb@narval.xyz", - "id": "credentialId3", - "alg": "ES256K", - "pubKey": "0xab88c8785D0C00082dE75D801Fcb1d5066a6311e" - } - ], - "transfers": [] -} diff --git a/apps/authz/src/app/opa/poc/entities.json b/apps/authz/src/opa/poc/entities.json similarity index 100% rename from apps/authz/src/app/opa/poc/entities.json rename to apps/authz/src/opa/poc/entities.json diff --git a/apps/authz/src/app/opa/poc/input_admin_transfer_token.json b/apps/authz/src/opa/poc/input_admin_transfer_token.json similarity index 100% rename from apps/authz/src/app/opa/poc/input_admin_transfer_token.json rename to apps/authz/src/opa/poc/input_admin_transfer_token.json diff --git a/apps/authz/src/app/opa/poc/input_call_crypto_unicorn_smart_contract.json b/apps/authz/src/opa/poc/input_call_crypto_unicorn_smart_contract.json similarity index 100% rename from apps/authz/src/app/opa/poc/input_call_crypto_unicorn_smart_contract.json rename to apps/authz/src/opa/poc/input_call_crypto_unicorn_smart_contract.json diff --git a/apps/authz/src/app/opa/poc/main.rego b/apps/authz/src/opa/poc/main.rego similarity index 100% rename from apps/authz/src/app/opa/poc/main.rego rename to apps/authz/src/opa/poc/main.rego diff --git a/apps/authz/src/app/opa/poc/main_test.rego b/apps/authz/src/opa/poc/main_test.rego similarity index 100% rename from apps/authz/src/app/opa/poc/main_test.rego rename to apps/authz/src/opa/poc/main_test.rego diff --git a/apps/authz/src/app/opa/poc/meta_permissions/user_permissions.json b/apps/authz/src/opa/poc/meta_permissions/user_permissions.json similarity index 100% rename from apps/authz/src/app/opa/poc/meta_permissions/user_permissions.json rename to apps/authz/src/opa/poc/meta_permissions/user_permissions.json diff --git a/apps/authz/src/app/opa/poc/meta_permissions/user_permissions.rego b/apps/authz/src/opa/poc/meta_permissions/user_permissions.rego similarity index 100% rename from apps/authz/src/app/opa/poc/meta_permissions/user_permissions.rego rename to apps/authz/src/opa/poc/meta_permissions/user_permissions.rego diff --git a/apps/authz/src/app/opa/poc/meta_permissions/user_permissions_test.rego b/apps/authz/src/opa/poc/meta_permissions/user_permissions_test.rego similarity index 100% rename from apps/authz/src/app/opa/poc/meta_permissions/user_permissions_test.rego rename to apps/authz/src/opa/poc/meta_permissions/user_permissions_test.rego diff --git a/apps/authz/src/app/opa/rego/__test__/criteria/accumulation_test.rego b/apps/authz/src/opa/rego/__test__/criteria/accumulation_test.rego similarity index 100% rename from apps/authz/src/app/opa/rego/__test__/criteria/accumulation_test.rego rename to apps/authz/src/opa/rego/__test__/criteria/accumulation_test.rego diff --git a/apps/authz/src/app/opa/rego/__test__/criteria/approvals_test.rego b/apps/authz/src/opa/rego/__test__/criteria/approvals_test.rego similarity index 100% rename from apps/authz/src/app/opa/rego/__test__/criteria/approvals_test.rego rename to apps/authz/src/opa/rego/__test__/criteria/approvals_test.rego diff --git a/apps/authz/src/app/opa/rego/__test__/criteria/destination_test.rego b/apps/authz/src/opa/rego/__test__/criteria/destination_test.rego similarity index 100% rename from apps/authz/src/app/opa/rego/__test__/criteria/destination_test.rego rename to apps/authz/src/opa/rego/__test__/criteria/destination_test.rego diff --git a/apps/authz/src/app/opa/rego/__test__/criteria/principal_test.rego b/apps/authz/src/opa/rego/__test__/criteria/principal_test.rego similarity index 100% rename from apps/authz/src/app/opa/rego/__test__/criteria/principal_test.rego rename to apps/authz/src/opa/rego/__test__/criteria/principal_test.rego diff --git a/apps/authz/src/app/opa/rego/__test__/criteria/resource_test.rego b/apps/authz/src/opa/rego/__test__/criteria/resource_test.rego similarity index 100% rename from apps/authz/src/app/opa/rego/__test__/criteria/resource_test.rego rename to apps/authz/src/opa/rego/__test__/criteria/resource_test.rego diff --git a/apps/authz/src/app/opa/rego/__test__/criteria/root_test.rego b/apps/authz/src/opa/rego/__test__/criteria/root_test.rego similarity index 100% rename from apps/authz/src/app/opa/rego/__test__/criteria/root_test.rego rename to apps/authz/src/opa/rego/__test__/criteria/root_test.rego diff --git a/apps/authz/src/app/opa/rego/__test__/criteria/source_test.rego b/apps/authz/src/opa/rego/__test__/criteria/source_test.rego similarity index 100% rename from apps/authz/src/app/opa/rego/__test__/criteria/source_test.rego rename to apps/authz/src/opa/rego/__test__/criteria/source_test.rego diff --git a/apps/authz/src/app/opa/rego/__test__/criteria/transfer_token_test.rego b/apps/authz/src/opa/rego/__test__/criteria/transfer_token_test.rego similarity index 100% rename from apps/authz/src/app/opa/rego/__test__/criteria/transfer_token_test.rego rename to apps/authz/src/opa/rego/__test__/criteria/transfer_token_test.rego diff --git a/apps/authz/src/app/opa/rego/__test__/main_test.rego b/apps/authz/src/opa/rego/__test__/main_test.rego similarity index 100% rename from apps/authz/src/app/opa/rego/__test__/main_test.rego rename to apps/authz/src/opa/rego/__test__/main_test.rego diff --git a/apps/authz/src/app/opa/rego/__test__/policies/spendings_test.rego b/apps/authz/src/opa/rego/__test__/policies/spendings_test.rego similarity index 100% rename from apps/authz/src/app/opa/rego/__test__/policies/spendings_test.rego rename to apps/authz/src/opa/rego/__test__/policies/spendings_test.rego diff --git a/apps/authz/src/app/opa/rego/__test__/utils/data_test.rego b/apps/authz/src/opa/rego/__test__/utils/data_test.rego similarity index 100% rename from apps/authz/src/app/opa/rego/__test__/utils/data_test.rego rename to apps/authz/src/opa/rego/__test__/utils/data_test.rego diff --git a/apps/authz/src/app/opa/rego/data.json b/apps/authz/src/opa/rego/data.json similarity index 100% rename from apps/authz/src/app/opa/rego/data.json rename to apps/authz/src/opa/rego/data.json diff --git a/apps/authz/src/opa/rego/input.json b/apps/authz/src/opa/rego/input.json new file mode 100644 index 000000000..8e73ef796 --- /dev/null +++ b/apps/authz/src/opa/rego/input.json @@ -0,0 +1,100 @@ +{ + "action": "signTransaction", + "intent": { + "to": "eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4", + "from": "eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b", + "type": "transferNative", + "amount": "1000000000000000000", + "token": "eip155:137/slip44/966" + }, + "transactionRequest": { + "from": "0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b", + "to": "0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4", + "chainId": 137, + "value": "0xde0b6b3a7640000", + "data": "0x00000000", + "nonce": 192, + "type": "2" + }, + "principal": { + "id": "credentialId1", + "alg": "ES256K", + "userId": "matt@narval.xyz", + "pubKey": "0xd75D626a116D4a1959fE3bB938B2e7c116A05890" + }, + "resource": { "uid": "eip155:eoa:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b" }, + "approvals": [ + { + "userId": "matt@narval.xyz", + "id": "credentialId1", + "alg": "ES256K", + "pubKey": "0xd75D626a116D4a1959fE3bB938B2e7c116A05890" + }, + { + "userId": "aa@narval.xyz", + "id": "credentialId2", + "alg": "ES256K", + "pubKey": "0x501D5c2Ce1EF208aadf9131a98BAa593258CfA06" + }, + { + "userId": "bb@narval.xyz", + "id": "credentialId3", + "alg": "ES256K", + "pubKey": "0xab88c8785D0C00082dE75D801Fcb1d5066a6311e" + } + ], + "transfers": [ + { + "amount": "3000000000", + "from": "eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b", + "to": "eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4", + "chainId": 137, + "token": "eip155:137/slip44/966", + "rates": { + "fiat:usd": "0.99", + "fiat:eur": "1.10" + }, + "initiatedBy": "matt@narval.xyz", + "timestamp": 1705934992613 + }, + { + "amount": "2000000000", + "from": "eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b", + "to": "eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4", + "chainId": 137, + "token": "eip155:137/slip44/966", + "rates": { + "fiat:usd": "0.99", + "fiat:eur": "1.10" + }, + "initiatedBy": "matt@narval.xyz", + "timestamp": 1705934992613 + }, + { + "amount": "1500000000", + "from": "eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b", + "to": "eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4", + "chainId": 137, + "token": "eip155:137/slip44/966", + "rates": { + "fiat:usd": "0.99", + "fiat:eur": "1.10" + }, + "initiatedBy": "matt@narval.xyz", + "timestamp": 1705934992613 + }, + { + "amount": "1000000000", + "from": "eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b", + "to": "eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4", + "chainId": 137, + "token": "eip155:137/slip44/966", + "rates": { + "fiat:usd": "0.99", + "fiat:eur": "1.10" + }, + "initiatedBy": "matt@narval.xyz", + "timestamp": 1705934992613 + } + ] +} diff --git a/apps/authz/src/app/opa/rego/lib/criteria/accumulation.rego b/apps/authz/src/opa/rego/lib/criteria/accumulation.rego similarity index 100% rename from apps/authz/src/app/opa/rego/lib/criteria/accumulation.rego rename to apps/authz/src/opa/rego/lib/criteria/accumulation.rego diff --git a/apps/authz/src/app/opa/rego/lib/criteria/approvals.rego b/apps/authz/src/opa/rego/lib/criteria/approvals.rego similarity index 96% rename from apps/authz/src/app/opa/rego/lib/criteria/approvals.rego rename to apps/authz/src/opa/rego/lib/criteria/approvals.rego index 92ae9fbd5..66f8875a7 100644 --- a/apps/authz/src/app/opa/rego/lib/criteria/approvals.rego +++ b/apps/authz/src/opa/rego/lib/criteria/approvals.rego @@ -2,20 +2,15 @@ package main import future.keywords.in -approvals := input.approvals - usersEntities := data.entities.users userGroupsEntities := data.entities.userGroups getApprovalsCount(possibleApprovers) = result { - approval := approvals[_] - matchedApprovers := {approval.userId | - approval := approvals[_] + approval := input.approvals[_] approval.userId in possibleApprovers } - result := count(matchedApprovers) } diff --git a/apps/authz/src/app/opa/rego/lib/criteria/destination.rego b/apps/authz/src/opa/rego/lib/criteria/destination.rego similarity index 100% rename from apps/authz/src/app/opa/rego/lib/criteria/destination.rego rename to apps/authz/src/opa/rego/lib/criteria/destination.rego diff --git a/apps/authz/src/app/opa/rego/lib/criteria/principal.rego b/apps/authz/src/opa/rego/lib/criteria/principal.rego similarity index 100% rename from apps/authz/src/app/opa/rego/lib/criteria/principal.rego rename to apps/authz/src/opa/rego/lib/criteria/principal.rego diff --git a/apps/authz/src/app/opa/rego/lib/criteria/resource.rego b/apps/authz/src/opa/rego/lib/criteria/resource.rego similarity index 100% rename from apps/authz/src/app/opa/rego/lib/criteria/resource.rego rename to apps/authz/src/opa/rego/lib/criteria/resource.rego diff --git a/apps/authz/src/app/opa/rego/lib/criteria/root.rego b/apps/authz/src/opa/rego/lib/criteria/root.rego similarity index 100% rename from apps/authz/src/app/opa/rego/lib/criteria/root.rego rename to apps/authz/src/opa/rego/lib/criteria/root.rego diff --git a/apps/authz/src/app/opa/rego/lib/criteria/source.rego b/apps/authz/src/opa/rego/lib/criteria/source.rego similarity index 100% rename from apps/authz/src/app/opa/rego/lib/criteria/source.rego rename to apps/authz/src/opa/rego/lib/criteria/source.rego diff --git a/apps/authz/src/app/opa/rego/lib/criteria/transferToken.rego b/apps/authz/src/opa/rego/lib/criteria/transferToken.rego similarity index 100% rename from apps/authz/src/app/opa/rego/lib/criteria/transferToken.rego rename to apps/authz/src/opa/rego/lib/criteria/transferToken.rego diff --git a/apps/authz/src/app/opa/rego/lib/main.rego b/apps/authz/src/opa/rego/lib/main.rego similarity index 100% rename from apps/authz/src/app/opa/rego/lib/main.rego rename to apps/authz/src/opa/rego/lib/main.rego diff --git a/apps/authz/src/app/opa/rego/lib/utils/data.rego b/apps/authz/src/opa/rego/lib/utils/data.rego similarity index 100% rename from apps/authz/src/app/opa/rego/lib/utils/data.rego rename to apps/authz/src/opa/rego/lib/utils/data.rego diff --git a/apps/authz/src/app/opa/rego/lib/utils/time.rego b/apps/authz/src/opa/rego/lib/utils/time.rego similarity index 100% rename from apps/authz/src/app/opa/rego/lib/utils/time.rego rename to apps/authz/src/opa/rego/lib/utils/time.rego diff --git a/apps/authz/src/app/opa/rego/policies/e2e.rego b/apps/authz/src/opa/rego/policies/e2e.rego similarity index 100% rename from apps/authz/src/app/opa/rego/policies/e2e.rego rename to apps/authz/src/opa/rego/policies/e2e.rego diff --git a/apps/authz/src/app/opa/rego/policies/policy1.rego b/apps/authz/src/opa/rego/policies/policy1.rego similarity index 99% rename from apps/authz/src/app/opa/rego/policies/policy1.rego rename to apps/authz/src/opa/rego/policies/policy1.rego index 7bacb5985..202df8d42 100644 --- a/apps/authz/src/app/opa/rego/policies/policy1.rego +++ b/apps/authz/src/opa/rego/policies/policy1.rego @@ -4,22 +4,17 @@ import future.keywords.in permit[{"policyId": "test-policy-1"}] := reason { checkPrincipal - input.action == "signTransaction" - checkTransferTokenType({"transferERC20"}) checkTransferTokenAddress({"eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174"}) checkTransferTokenOperation({"operator": "lte", "value": "1000000000000000000"}) - approvalsRequired = [{ "approvalCount": 2, "countPrincipal": false, "approvalEntityType": "Narval::User", "entityIds": ["test-bob-uid", "test-bar-uid", "test-signer-uid"], }] - approvals := getApprovalsResult(approvalsRequired) - reason := { "type": "permit", "policyId": "test-policy-1", diff --git a/apps/authz/src/app/opa/rego/policies/policy2.rego b/apps/authz/src/opa/rego/policies/policy2.rego similarity index 99% rename from apps/authz/src/app/opa/rego/policies/policy2.rego rename to apps/authz/src/opa/rego/policies/policy2.rego index 408e4321c..02dfd7d84 100644 --- a/apps/authz/src/app/opa/rego/policies/policy2.rego +++ b/apps/authz/src/opa/rego/policies/policy2.rego @@ -4,22 +4,17 @@ import future.keywords.in permit[{"policyId": "test-policy-2"}] := reason { checkPrincipal - input.action == "signTransaction" - checkTransferTokenType({"transferERC20"}) checkTransferTokenAddress({"eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174"}) checkTransferTokenOperation({"operator": "lte", "value": "1000000000000000000"}) - approvalsRequired = [{ "approvalCount": 2, "countPrincipal": false, "approvalEntityType": "Narval::UserGroup", "entityIds": ["test-user-group-one-uid"], }] - approvals := getApprovalsResult(approvalsRequired) - reason := { "type": "permit", "policyId": "test-policy-2", diff --git a/apps/authz/src/app/opa/rego/policies/policy3.rego b/apps/authz/src/opa/rego/policies/policy3.rego similarity index 99% rename from apps/authz/src/app/opa/rego/policies/policy3.rego rename to apps/authz/src/opa/rego/policies/policy3.rego index 5e1c6fccf..63f66b9eb 100644 --- a/apps/authz/src/app/opa/rego/policies/policy3.rego +++ b/apps/authz/src/opa/rego/policies/policy3.rego @@ -4,22 +4,17 @@ import future.keywords.in permit[{"policyId": "test-policy-3"}] := reason { checkPrincipal - input.action == "signTransaction" - checkTransferTokenType({"transferERC20"}) checkTransferTokenAddress({"eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174"}) checkTransferTokenOperation({"operator": "lte", "value": "1000000000000000000"}) - approvalsRequired = [{ "approvalCount": 2, "countPrincipal": false, "approvalEntityType": "Narval::UserRole", "entityIds": ["root", "admin"], }] - approvals := getApprovalsResult(approvalsRequired) - reason := { "type": "permit", "policyId": "test-policy-3", diff --git a/apps/authz/src/app/opa/rego/policies/spendings.rego b/apps/authz/src/opa/rego/policies/spendings.rego similarity index 99% rename from apps/authz/src/app/opa/rego/policies/spendings.rego rename to apps/authz/src/opa/rego/policies/spendings.rego index 47bb96c7f..ebe94e730 100644 --- a/apps/authz/src/app/opa/rego/policies/spendings.rego +++ b/apps/authz/src/opa/rego/policies/spendings.rego @@ -6,22 +6,17 @@ import future.keywords.in forbid[{"policyId": "test-accumulation-policy-1"}] := reason { checkPrincipal - input.action == "signTransaction" - transferTypes = {"transferERC20"} roles = {"member"} tokens = {"eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174", "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"} limit = to_number("5000000000") startDate = secondsToNanoSeconds(nowSeconds - ((12 * 60) * 60)) - checkPrincipalRole(roles) checkTransferTokenType(transferTypes) checkTransferTokenAddress(tokens) - spendings = getUsdSpendingAmount({"tokens": tokens, "startDate": startDate}) checkSpendingLimitReached(spendings, transferTokenAmount, limit) - reason := { "type": "forbid", "policyId": "test-accumulation-policy-1", @@ -34,19 +29,15 @@ forbid[{"policyId": "test-accumulation-policy-1"}] := reason { forbid[{"policyId": "test-accumulation-policy-2"}] := reason { checkPrincipal - input.action == "signTransaction" - transferTypes = {"transferERC20"} users = {"test-alice-uid"} tokens = {"eip155:137/erc20:0x2791bca1f2de4661ed88a30c99a7a9449aa84174", "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"} limit = to_number("5000000000") startDate = secondsToNanoSeconds(nowSeconds - ((12 * 60) * 60)) - checkPrincipalId(users) checkTransferTokenType(transferTypes) checkTransferTokenAddress(tokens) - spendings = getUsdSpendingAmount({"tokens": tokens, "users": users, "startDate": startDate}) checkSpendingLimitReached(spendings, transferTokenAmount, limit) @@ -62,20 +53,15 @@ forbid[{"policyId": "test-accumulation-policy-2"}] := reason { forbid[{"policyId": "test-accumulation-policy-3"}] := reason { checkPrincipal - input.action == "signTransaction" - transferTypes = {"transferERC20"} resources = {"eip155:eoa:0xddcf208f219a6e6af072f2cfdc615b2c1805f98e"} limit = to_number("5000000000") startDate = secondsToNanoSeconds(nowSeconds - ((12 * 60) * 60)) - checkTransferTokenType(transferTypes) checkWalletId(resources) - spendings = getUsdSpendingAmount({"resources": resources, "startDate": startDate}) checkSpendingLimitReached(spendings, transferTokenAmount, limit) - reason := { "type": "forbid", "policyId": "test-accumulation-policy-3", @@ -89,17 +75,13 @@ forbid[{"policyId": "test-accumulation-policy-3"}] := reason { forbid[{"policyId": "test-accumulation-policy-4"}] := reason { checkPrincipal input.action == "signTransaction" - transferTypes = {"transferERC20"} userGroups = {"test-user-group-one-uid"} limit = to_number("5000000000") startDate = secondsToNanoSeconds(nowSeconds - ((24 * 60) * 60)) - checkTransferTokenType(transferTypes) - spendings = getUsdSpendingAmount({"userGroups": userGroups, "startDate": startDate}) checkSpendingLimitReached(spendings, transferTokenAmount, limit) - reason := { "type": "forbid", "policyId": "test-accumulation-policy-4", @@ -113,17 +95,13 @@ forbid[{"policyId": "test-accumulation-policy-4"}] := reason { forbid[{"policyId": "test-accumulation-policy-5"}] := reason { checkPrincipal input.action == "signTransaction" - transferTypes = {"transferERC20"} walletGroups = {"test-wallet-group-one-uid"} limit = to_number("5000000000") startDate = secondsToNanoSeconds(nowSeconds - ((24 * 60) * 60)) - checkTransferTokenType(transferTypes) - spendings = getUsdSpendingAmount({"walletGroups": walletGroups, "startDate": startDate}) checkSpendingLimitReached(spendings, transferTokenAmount, limit) - reason := { "type": "forbid", "policyId": "test-accumulation-policy-5", diff --git a/apps/authz/src/app/opa/rego/script.ts b/apps/authz/src/opa/rego/script.ts similarity index 76% rename from apps/authz/src/app/opa/rego/script.ts rename to apps/authz/src/opa/rego/script.ts index 6efb4819a..142fc792e 100644 --- a/apps/authz/src/app/opa/rego/script.ts +++ b/apps/authz/src/opa/rego/script.ts @@ -1,11 +1,12 @@ import { loadPolicy } from '@open-policy-agent/opa-wasm' -import fs from 'fs' +import { readFileSync } from 'fs' +import path from 'path' import policyData from './data.json' import policyInput from './input.json' -const policyWasm = fs.readFileSync('/Users/samuel/Documents/narval/narval/rego-build/policy.wasm') +const OPA_WASM_PATH = readFileSync(path.join(process.cwd(), './rego-build/policy.wasm')) -loadPolicy(policyWasm, undefined, { +loadPolicy(OPA_WASM_PATH, undefined, { 'time.now_ns': () => new Date().getTime() * 1000000 }) .then((policy) => { diff --git a/apps/authz/src/shared/module/persistence/mock_data.ts b/apps/authz/src/shared/module/persistence/mock_data.ts index 5b437500b..8412bfa02 100644 --- a/apps/authz/src/shared/module/persistence/mock_data.ts +++ b/apps/authz/src/shared/module/persistence/mock_data.ts @@ -3,7 +3,8 @@ import { Action, Alg, AuthCredential, - AuthZRequestPayload, + AuthorizationRequest, + AuthorizationRequestPayload, TransactionRequest, UserRoles } from '@app/authz/shared/types/domain.type' @@ -262,9 +263,9 @@ export const mockEntityData: RegoData = { // stub out the actual tx request & signature // This is what would be the initial input from the external service -export const generateInboundRequest = async (): Promise => { +export const generateInboundRequest = async (): Promise => { const txRequest = NATIVE_TRANSFER_TX_REQUEST - const request = { + const request: AuthorizationRequest = { action: Action.SIGN_TRANSACTION, nonce: 'random-nonce-111', transactionRequest: txRequest, diff --git a/apps/authz/src/shared/types/domain.type.ts b/apps/authz/src/shared/types/domain.type.ts index 8b15ad263..ab2482316 100644 --- a/apps/authz/src/shared/types/domain.type.ts +++ b/apps/authz/src/shared/types/domain.type.ts @@ -56,19 +56,53 @@ export type HistoricalTransfer = { token: Caip10 rates: { [keyof in FiatSymbols]: string } // eg. { fiat:usd: '0.01', fiat:eur: '0.02' } initiatedBy: string // uid of the user who initiated the spending - timestamp: number // unix timestamp + timestamp: number // unix timestamp in ms } +export type SharedAuthorizationRequest = { + action: Action + nonce: string +} + +export type SignTransaction = SharedAuthorizationRequest & { + action: Action.SIGN_TRANSACTION + resourceId: string + transactionRequest: TransactionRequest +} + +export type SignMessage = SharedAuthorizationRequest & { + action: Action.SIGN_MESSAGE + resourceId: string + message: string +} + +export type AuthorizationRequest = SignTransaction | SignMessage + /** - * The activity/data being authorized. This must include all the data being authorized, and nothing except the data being authorized. - * This is the data that will be hashed and signed. + * The action being authorized. + * + * This must include all the data being authorized, and nothing except the data + * being authorized. This is the data that will be hashed and signed. */ -export type AuthZRequest = { - action: Action - nonce: string // A unique nonce for this request, to prevent replay attacks - resourceId?: string - transactionRequest?: TransactionRequest // for signTransaction - message?: string // for signMessage +export type AuthorizationRequestPayload = { + /** + * The initiator signature of the request using `hashRequest` method to ensure + * SHA256 format. + */ + authentication: RequestSignature + /** + * The authorization request of + */ + request: AuthorizationRequest + /** + * List of approvals required by the policy. + */ + approvals?: RequestSignature[] + /** + * List of known approved transfers (not mined). These are used by policies on + * the history like spending limits. + */ + transfers?: HistoricalTransfer[] } /** @@ -80,13 +114,6 @@ export type RequestSignature = { pubKey: string // Depending on the alg, this may be necessary (e.g., RSA cannot recover the public key from the signature) } -export type AuthZRequestPayload = { - authentication: RequestSignature // The signature of the initiator - request: AuthZRequest - approvals?: RequestSignature[] // Other approvals, incl. second factors of the initiator - transfers?: HistoricalTransfer[] -} - export enum NarvalDecision { Permit = 'Permit', Forbid = 'Forbid', @@ -106,10 +133,10 @@ export type ApprovalRequirement = { countPrincipal: boolean } -export type AuthZResponse = { +export type AuthorizationResponse = { decision: NarvalDecision permitSignature?: RequestSignature // The ENGINE's approval signature - request?: AuthZRequest // The actual authorized request + request?: AuthorizationRequest // The actual authorized request totalApprovalsRequired?: ApprovalRequirement[] approvalsMissing?: ApprovalRequirement[] approvalsSatisfied?: ApprovalRequirement[] diff --git a/apps/orchestration/project.json b/apps/orchestration/project.json index c5503eb91..cc4cbb5e1 100644 --- a/apps/orchestration/project.json +++ b/apps/orchestration/project.json @@ -18,10 +18,19 @@ "webpackConfig": "apps/orchestration/webpack.config.js" }, "configurations": { + "repl": { + "main": "apps/orchestration/src/repl.ts" + }, "development": {}, "production": {} } }, + "repl": { + "executor": "@nx/node:node", + "options": { + "buildTarget": "orchestration:build:repl" + } + }, "serve": { "executor": "@nx/js:node", "defaultConfiguration": "development", diff --git a/apps/orchestration/src/policy-engine/__test__/e2e/facade.spec.ts b/apps/orchestration/src/policy-engine/__test__/e2e/facade.spec.ts index b7a4f9841..0f91186b3 100644 --- a/apps/orchestration/src/policy-engine/__test__/e2e/facade.spec.ts +++ b/apps/orchestration/src/policy-engine/__test__/e2e/facade.spec.ts @@ -10,7 +10,6 @@ import { PolicyEngineModule } from '@app/orchestration/policy-engine/policy-engi import { PersistenceModule } from '@app/orchestration/shared/module/persistence/persistence.module' import { TestPrismaService } from '@app/orchestration/shared/module/persistence/service/test-prisma.service' import { QueueModule } from '@app/orchestration/shared/module/queue/queue.module' -import { TransactionType, hashRequest } from '@narval/authz-shared' import { getQueueToken } from '@nestjs/bull' import { HttpStatus, INestApplication } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' @@ -96,15 +95,15 @@ describe('Policy Engine Cluster Facade', () => { describe('POST /evaluations', () => { it('evaluates a sign message authorization request', async () => { - const signMessageRequest = { - message: 'Sign me, please' - } const payload = { - action: SupportedAction.SIGN_MESSAGE, - request: signMessageRequest, - hash: hashRequest(signMessageRequest), authentication, - approvals + approvals, + request: { + action: SupportedAction.SIGN_MESSAGE, + nonce: '99', + resourceId: '5cfb8614-ddeb-4764-bf85-8d323f26d3b3', + message: 'Sign me, please' + } } const { status, body } = await request(app.getHttpServer()) @@ -112,42 +111,44 @@ describe('Policy Engine Cluster Facade', () => { .set(REQUEST_HEADER_ORG_ID, org.id) .send(payload) - expect(status).toEqual(HttpStatus.OK) expect(body).toMatchObject({ + approvals, id: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), status: AuthorizationRequestStatus.CREATED, idempotencyKey: null, - action: payload.action, - hash: payload.hash, request: payload.request, - createdAt: expect.any(String), - updatedAt: expect.any(String) + evaluations: [] }) + expect(status).toEqual(HttpStatus.OK) }) it('evaluates a sign transaction authorization request', async () => { - const signTransactionRequest = { - chainId: 1, - data: '0x', - from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', - gas: '5000', - nonce: 0, - to: '0xbbb7be636c3ad8cf9d08ba8bdba4abd2ef29bd23', - type: TransactionType.EIP1559, - value: '0x', - accessList: [ - { - address: '0xccc1472fce4ec74a1e3f9653776acfc790cd0743', - storageKeys: [stringToHex('storage-key-one'), stringToHex('storage-key-two')] - } - ] - } const payload = { - action: SupportedAction.SIGN_TRANSACTION, - hash: hashRequest(signTransactionRequest), - request: signTransactionRequest, authentication, - approvals + approvals, + request: { + action: SupportedAction.SIGN_TRANSACTION, + nonce: '99', + resourceId: '68dc69bd-87d2-49d9-a5de-f482507b25c2', + transactionRequest: { + chainId: 1, + data: '0x', + from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', + gas: '5000', + nonce: 0, + to: '0xbbb7be636c3ad8cf9d08ba8bdba4abd2ef29bd23', + type: '2', + value: '0x', + accessList: [ + { + address: '0xccc1472fce4ec74a1e3f9653776acfc790cd0743', + storageKeys: [stringToHex('storage-key-one'), stringToHex('storage-key-two')] + } + ] + } + } } const { status, body } = await request(app.getHttpServer()) @@ -156,27 +157,29 @@ describe('Policy Engine Cluster Facade', () => { .send(payload) expect(body).toMatchObject({ + approvals, id: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), status: AuthorizationRequestStatus.CREATED, idempotencyKey: null, - action: payload.action, - hash: payload.hash, request: payload.request, - createdAt: expect.any(String), - updatedAt: expect.any(String) + evaluations: [] }) expect(status).toEqual(HttpStatus.OK) }) it('evaluates a partial sign transaction authorization request', async () => { - const signTransactionRequest = { - from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', - chainId: 1 - } const payload = { - action: SupportedAction.SIGN_TRANSACTION, - hash: hashRequest(signTransactionRequest), - request: signTransactionRequest, + request: { + action: SupportedAction.SIGN_TRANSACTION, + nonce: '99', + resourceId: '68dc69bd-87d2-49d9-a5de-f482507b25c2', + transactionRequest: { + from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', + chainId: 1 + } + }, authentication, approvals } @@ -187,31 +190,31 @@ describe('Policy Engine Cluster Facade', () => { .send(payload) expect(body).toMatchObject({ + approvals, id: expect.any(String), + createdAt: expect.any(String), + updatedAt: expect.any(String), status: AuthorizationRequestStatus.CREATED, idempotencyKey: null, - action: payload.action, - hash: payload.hash, request: payload.request, - createdAt: expect.any(String), - updatedAt: expect.any(String) + evaluations: [] }) expect(status).toEqual(HttpStatus.OK) }) }) describe('GET /evaluations/:id', () => { - const signMessageRequest = { - message: 'Testing sign message request' - } const authzRequest: AuthorizationRequest = { authentication, id: '986ae19d-c30c-40c6-b873-1fb6c49011de', orgId: org.id, status: AuthorizationRequestStatus.PERMITTED, - action: SupportedAction.SIGN_MESSAGE, - request: signMessageRequest, - hash: hashRequest(signMessageRequest), + request: { + action: SupportedAction.SIGN_MESSAGE, + nonce: '99', + resourceId: '5cfb8614-ddeb-4764-bf85-8d323f26d3b3', + message: 'Testing sign message request' + }, idempotencyKey: '8dcbb7ad-82a2-4eca-b2f0-b1415c1d4a17', evaluations: [], approvals: [], diff --git a/apps/orchestration/src/policy-engine/core/service/__test__/unit/authorization-request.service.spec.ts b/apps/orchestration/src/policy-engine/core/service/__test__/unit/authorization-request.service.spec.ts new file mode 100644 index 000000000..167a0fc65 --- /dev/null +++ b/apps/orchestration/src/policy-engine/core/service/__test__/unit/authorization-request.service.spec.ts @@ -0,0 +1,92 @@ +import { AuthorizationRequestService } from '@app/orchestration/policy-engine/core/service/authorization-request.service' +import { Approval, AuthorizationRequest, SupportedAction } from '@app/orchestration/policy-engine/core/type/domain.type' +import { AuthorizationRequestRepository } from '@app/orchestration/policy-engine/persistence/repository/authorization-request.repository' +import { AuthorizationRequestProcessingProducer } from '@app/orchestration/policy-engine/queue/producer/authorization-request-processing.producer' +import { createMock } from '@golevelup/ts-jest' +import { HttpService } from '@nestjs/axios' +import { Test, TestingModule } from '@nestjs/testing' +import { AuthorizationRequestStatus } from '@prisma/client/orchestration' + +describe(AuthorizationRequestService.name, () => { + let module: TestingModule + let authzRequestRepositoryMock: AuthorizationRequestRepository + let authzRequestProcessingProducerMock: AuthorizationRequestProcessingProducer + let httpServiceMock: HttpService + let service: AuthorizationRequestService + + const authzRequest: AuthorizationRequest = { + authentication: { + sig: '0xe24d097cea880a40f8be2cf42f497b9fbda5f9e4a31b596827e051d78dce75c032fa7e5ee3046f7c6f116e5b98cb8d268fa9b9d222ff44719e2ec2a0d9159d0d1c', + alg: 'ES256K', + pubKey: '0xd75D626a116D4a1959fE3bB938B2e7c116A05890' + }, + id: '6c7e92fc-d2b0-4840-8e9b-485393ecdf89', + orgId: '', + status: AuthorizationRequestStatus.PROCESSING, + request: { + action: SupportedAction.SIGN_MESSAGE, + nonce: '99', + resourceId: '239bb48b-f708-47ba-97fa-ef336be4dffe', + message: 'Test request' + }, + idempotencyKey: null, + approvals: [], + evaluations: [], + createdAt: new Date(), + updatedAt: new Date() + } + + beforeEach(async () => { + authzRequestRepositoryMock = createMock() + authzRequestProcessingProducerMock = createMock() + httpServiceMock = createMock() + + module = await Test.createTestingModule({ + providers: [ + AuthorizationRequestService, + { + provide: AuthorizationRequestRepository, + useValue: authzRequestRepositoryMock + }, + { + provide: AuthorizationRequestProcessingProducer, + useValue: authzRequestProcessingProducerMock + }, + { + provide: HttpService, + useValue: httpServiceMock + } + ] + }).compile() + + service = module.get(AuthorizationRequestService) + }) + + describe('approve', () => { + const approval: Approval = { + id: '3cf9f630-e621-494a-825c-5af917dc3a5e', + sig: '0xcc645f43d8df80c4deeb2e60a8c0c15d58586d2c29ea7c85208cea81d1c47cbd787b1c8473dde70c3a7d49f573e491223107933257b2b99ecc4806b7cc16848d1c', + alg: 'ES256K', + pubKey: '0xab88c8785D0C00082dE75D801Fcb1d5066a6311e', + createdAt: new Date() + } + + const updatedAuthzRequest: AuthorizationRequest = { + ...authzRequest, + approvals: [approval] + } + + it('creates a new approval and evaluates the authorization request', async () => { + jest.spyOn(authzRequestRepositoryMock, 'update').mockResolvedValue(updatedAuthzRequest) + jest.spyOn(service, 'evaluate').mockResolvedValue(updatedAuthzRequest) + + await service.approve(authzRequest.id, approval) + + expect(authzRequestRepositoryMock.update).toHaveBeenCalledWith({ + id: authzRequest.id, + approvals: [approval] + }) + expect(service.evaluate).toHaveBeenCalledWith(updatedAuthzRequest) + }) + }) +}) diff --git a/apps/orchestration/src/policy-engine/core/service/authorization-request.service.ts b/apps/orchestration/src/policy-engine/core/service/authorization-request.service.ts index 658396e75..d9a9f309f 100644 --- a/apps/orchestration/src/policy-engine/core/service/authorization-request.service.ts +++ b/apps/orchestration/src/policy-engine/core/service/authorization-request.service.ts @@ -1,4 +1,5 @@ import { + Approval, AuthorizationRequest, AuthorizationRequestStatus, CreateAuthorizationRequest @@ -8,8 +9,10 @@ import { AuthorizationRequestProcessingProducer } from '@app/orchestration/polic import { ApplicationException } from '@app/orchestration/shared/exception/application.exception' import { HttpService } from '@nestjs/axios' import { HttpStatus, Injectable, Logger } from '@nestjs/common' -import { catchError, delay, lastValueFrom, map, switchMap, tap } from 'rxjs' +import { catchError, lastValueFrom, map, switchMap, tap } from 'rxjs' +import { SetOptional } from 'type-fest' import { v4 as uuid } from 'uuid' +import { getOkTransfers } from './transfers.mock' const getStatus = (decision: string): AuthorizationRequestStatus => { const statuses: Map = new Map([ @@ -77,17 +80,43 @@ export class AuthorizationRequestService { }) } + async approve(id: string, approval: SetOptional): Promise { + const authzRequest = await this.authzRequestRepository.update({ + id: id, + approvals: [ + { + id: approval.id || uuid(), + createdAt: approval.createdAt || new Date(), + ...approval + } + ] + }) + + return this.evaluate(authzRequest) + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars async complete(id: string) {} async evaluate(input: AuthorizationRequest): Promise { + // TODO (@wcalderipe, 19/01/24): Think how to error the evaluation but + // short-circuit the retry mechanism. + + const payload = { + authentication: input.authentication, + approvals: input.approvals, + request: input.request, + // transfers: getNotOkTransfers() + transfers: getOkTransfers() + } + this.logger.log('Sending authorization request to cluster evaluation', { - input + authzRequest: input, + payload }) return lastValueFrom( - this.httpService.post('http://localhost:3010/evaluation', input).pipe( - delay(3000), // fake some delay + this.httpService.post('http://localhost:3010/evaluation', payload).pipe( tap((response) => { this.logger.log('Received evaluation response', { status: response.status, @@ -104,7 +133,7 @@ export class AuthorizationRequestService { { id: uuid(), decision: evaluation.decision, - signature: evaluation.permitSignature, + signature: evaluation?.permitSignature?.sig || null, createdAt: new Date() } ] diff --git a/apps/orchestration/src/policy-engine/core/service/transfers.mock.ts b/apps/orchestration/src/policy-engine/core/service/transfers.mock.ts new file mode 100644 index 000000000..c15a7f04a --- /dev/null +++ b/apps/orchestration/src/policy-engine/core/service/transfers.mock.ts @@ -0,0 +1,130 @@ +import { getTime, subHours } from 'date-fns' + +const getTimestamps = () => { + const now = new Date() + + return { + twenty_hours_ago: getTime(subHours(now, 20)), + eleven_hours_ago: getTime(subHours(now, 11)), + ten_hours_ago: getTime(subHours(now, 10)), + nine_hours_ago: getTime(subHours(now, 9)) + } +} + +export const getOkTransfers = () => { + const timestamps = getTimestamps() + + return [ + { + amount: '3000000000', + from: 'eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', + to: 'eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + chainId: 137, + token: 'eip155:137/slip44/966', + rates: { + 'fiat:usd': '0.99', + 'fiat:eur': '1.10' + }, + initiatedBy: 'matt@narval.xyz', + timestamp: timestamps.twenty_hours_ago + }, + { + amount: '2000000000', + from: 'eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', + to: 'eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + chainId: 137, + token: 'eip155:137/slip44/966', + rates: { + 'fiat:usd': '0.99', + 'fiat:eur': '1.10' + }, + initiatedBy: 'matt@narval.xyz', + timestamp: timestamps.twenty_hours_ago + }, + { + amount: '1500000000', + from: 'eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', + to: 'eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + chainId: 137, + token: 'eip155:137/slip44/966', + rates: { + 'fiat:usd': '0.99', + 'fiat:eur': '1.10' + }, + initiatedBy: 'matt@narval.xyz', + timestamp: timestamps.twenty_hours_ago + }, + { + amount: '1000000000', + from: 'eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', + to: 'eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + chainId: 137, + token: 'eip155:137/slip44/966', + rates: { + 'fiat:usd': '0.99', + 'fiat:eur': '1.10' + }, + initiatedBy: 'matt@narval.xyz', + timestamp: timestamps.twenty_hours_ago + } + ] +} + +export const getNotOkTransfers = () => { + const timestamps = getTimestamps() + + return [ + { + amount: '3000000000', + from: 'eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', + to: 'eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + chainId: 137, + token: 'eip155:137/slip44/966', + rates: { + 'fiat:usd': '0.99', + 'fiat:eur': '1.10' + }, + initiatedBy: 'matt@narval.xyz', + timestamp: timestamps.eleven_hours_ago + }, + { + amount: '2000000000', + from: 'eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', + to: 'eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + chainId: 137, + token: 'eip155:137/slip44/966', + rates: { + 'fiat:usd': '0.99', + 'fiat:eur': '1.10' + }, + initiatedBy: 'matt@narval.xyz', + timestamp: timestamps.eleven_hours_ago + }, + { + amount: '1500000000', + from: 'eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', + to: 'eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + chainId: 137, + token: 'eip155:137/slip44/966', + rates: { + 'fiat:usd': '0.99', + 'fiat:eur': '1.10' + }, + initiatedBy: 'matt@narval.xyz', + timestamp: timestamps.eleven_hours_ago + }, + { + amount: '1000000000', + from: 'eip155:137:0x90d03a8971a2faa19a9d7ffdcbca28fe826a289b', + to: 'eip155:137:0x08a08d0504d4f3363a5b7fda1f5fff1c7bca8ad4', + chainId: 137, + token: 'eip155:137/slip44/966', + rates: { + 'fiat:usd': '0.99', + 'fiat:eur': '1.10' + }, + initiatedBy: 'matt@narval.xyz', + timestamp: timestamps.eleven_hours_ago + } + ] +} diff --git a/apps/orchestration/src/policy-engine/core/type/domain.type.ts b/apps/orchestration/src/policy-engine/core/type/domain.type.ts index 3fbf94f5f..460c80d29 100644 --- a/apps/orchestration/src/policy-engine/core/type/domain.type.ts +++ b/apps/orchestration/src/policy-engine/core/type/domain.type.ts @@ -37,41 +37,37 @@ export type Evaluation = { createdAt: Date } -export type SharedAuthorizationRequest = { - id: string - orgId: string - status: `${AuthorizationRequestStatus}` - /** - * The hash of the request in EIP-191 format. - * - * @see https://eips.ethereum.org/EIPS/eip-191 - * @see https://viem.sh/docs/utilities/hashMessage.html - * @see https://docs.ethers.org/v5/api/utils/hashing/ - */ - hash: string - idempotencyKey?: string | null - authentication: Signature - approvals: Approval[] - evaluations: Evaluation[] - createdAt: Date - updatedAt: Date +export type SharedAuthorizationPayload = { + action: `${SupportedAction}` + nonce: string } -export type SignTransactionAuthorizationRequest = SharedAuthorizationRequest & { +export type SignTransaction = SharedAuthorizationPayload & { action: `${SupportedAction.SIGN_TRANSACTION}` - request: TransactionRequest + resourceId: string + transactionRequest: TransactionRequest } -export type MessageRequest = { +export type SignMessage = SharedAuthorizationPayload & { + action: `${SupportedAction.SIGN_MESSAGE}` + resourceId: string message: string } -export type SignMessageAuthorizationRequest = SharedAuthorizationRequest & { - action: `${SupportedAction.SIGN_MESSAGE}` - request: MessageRequest -} +export type Request = SignTransaction | SignMessage -export type AuthorizationRequest = SignTransactionAuthorizationRequest | SignMessageAuthorizationRequest +export type AuthorizationRequest = { + id: string + orgId: string + status: `${AuthorizationRequestStatus}` + authentication: Signature + request: Request + approvals: Approval[] + evaluations: Evaluation[] + idempotencyKey?: string | null + createdAt: Date + updatedAt: Date +} export type CreateApproval = SetOptional @@ -82,14 +78,6 @@ export type CreateAuthorizationRequest = OverrideProperties< } > -export function isSignTransaction(request: AuthorizationRequest): request is SignTransactionAuthorizationRequest { - return (request as SignTransactionAuthorizationRequest).action === SupportedAction.SIGN_TRANSACTION -} - -export function isSignMessage(request: AuthorizationRequest): request is SignMessageAuthorizationRequest { - return (request as SignMessageAuthorizationRequest).action === SupportedAction.SIGN_MESSAGE -} - export type AuthorizationRequestProcessingJob = { id: string } diff --git a/apps/orchestration/src/policy-engine/http/rest/controller/facade.controller.ts b/apps/orchestration/src/policy-engine/http/rest/controller/facade.controller.ts index 89487f985..c5527afb9 100644 --- a/apps/orchestration/src/policy-engine/http/rest/controller/facade.controller.ts +++ b/apps/orchestration/src/policy-engine/http/rest/controller/facade.controller.ts @@ -1,6 +1,7 @@ import { AuthorizationRequestService } from '@app/orchestration/policy-engine/core/service/authorization-request.service' import { AuthorizationRequestDto } from '@app/orchestration/policy-engine/http/rest/dto/authorization-request.dto' import { AuthorizationResponseDto } from '@app/orchestration/policy-engine/http/rest/dto/authorization-response.dto' +import { SignatureDto } from '@app/orchestration/policy-engine/http/rest/dto/signature.dto' import { toCreateAuthorizationRequest } from '@app/orchestration/policy-engine/http/rest/util' import { OrgId } from '@app/orchestration/shared/decorator/org-id.decorator' import { Body, Controller, Get, HttpCode, HttpStatus, NotFoundException, Param, Post } from '@nestjs/common' @@ -21,6 +22,9 @@ export class FacadeController { async evaluation(@OrgId() orgId: string, @Body() body: AuthorizationRequestDto): Promise { const authzRequest = await this.authorizationRequestService.create(toCreateAuthorizationRequest(orgId, body)) + // TODO (@wcalderipe, 23/01/24): Validate if the signed hash is the same + // hash used internally. + return new AuthorizationResponseDto(authzRequest) } @@ -39,4 +43,11 @@ export class FacadeController { throw new NotFoundException('Authorization request not found') } + + @Post('/approve/:id') + async approve(@Param('id') id: string, @Body() body: SignatureDto): Promise { + const authzRequest = await this.authorizationRequestService.approve(id, body) + + return new AuthorizationResponseDto(authzRequest) + } } diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/authorization-request.dto.ts b/apps/orchestration/src/policy-engine/http/rest/dto/authorization-request.dto.ts index ddec6d1a1..ca1ca1b73 100644 --- a/apps/orchestration/src/policy-engine/http/rest/dto/authorization-request.dto.ts +++ b/apps/orchestration/src/policy-engine/http/rest/dto/authorization-request.dto.ts @@ -4,34 +4,34 @@ import { SignTransactionRequestDto } from '@app/orchestration/policy-engine/http import { SignatureDto } from '@app/orchestration/policy-engine/http/rest/dto/signature.dto' import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger' import { Type } from 'class-transformer' -import { IsDefined, IsEnum, IsString, Validate, ValidateNested } from 'class-validator' -import { RequestHash } from './validator/request-hash.validator' +import { IsDefined, ValidateNested } from 'class-validator' @ApiExtraModels(SignTransactionRequestDto, SignMessageRequestDto) export class AuthorizationRequestDto { - @IsEnum(SupportedAction) - @IsDefined() - @ApiProperty({ - enum: SupportedAction - }) - action: `${SupportedAction}` - @IsDefined() @ValidateNested() @ApiProperty() + @Type(() => SignatureDto) authentication: SignatureDto @IsDefined() @ValidateNested() + @Type(() => SignatureDto) @ApiProperty({ type: () => SignatureDto, isArray: true }) approvals: SignatureDto[] + // TODO (@wcalderipe, 22/01/24): Test the discrimination type option from + // class-transformer instead of a custom function map. + // + // See https://github.com/typestack/class-transformer?tab=readme-ov-file#working-with-nested-objects @ValidateNested() @Type((opts) => { - return opts?.object.action === SupportedAction.SIGN_TRANSACTION ? SignTransactionRequestDto : SignMessageRequestDto + return opts?.object.request.action === SupportedAction.SIGN_TRANSACTION + ? SignTransactionRequestDto + : SignMessageRequestDto }) @IsDefined() @ApiProperty({ @@ -39,20 +39,11 @@ export class AuthorizationRequestDto { }) request: SignTransactionRequestDto | SignMessageRequestDto - @IsString() - @IsDefined() - @Validate(RequestHash) - @ApiProperty({ - description: 'The hash of the request in EIP-191 format.', - required: true - }) - hash: string - isSignTransaction(request: SignTransactionRequestDto | SignMessageRequestDto): request is SignTransactionRequestDto { - return this.action === SupportedAction.SIGN_TRANSACTION + return this.request.action === SupportedAction.SIGN_TRANSACTION } isSignMessage(request: SignTransactionRequestDto | SignMessageRequestDto): request is SignMessageRequestDto { - return this.action === SupportedAction.SIGN_MESSAGE + return this.request.action === SupportedAction.SIGN_MESSAGE } } diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/authorization-response.dto.ts b/apps/orchestration/src/policy-engine/http/rest/dto/authorization-response.dto.ts index d273991f6..d4351e796 100644 --- a/apps/orchestration/src/policy-engine/http/rest/dto/authorization-response.dto.ts +++ b/apps/orchestration/src/policy-engine/http/rest/dto/authorization-response.dto.ts @@ -2,32 +2,30 @@ import { AuthorizationRequestStatus, SupportedAction } from '@app/orchestration/ import { EvaluationDto } from '@app/orchestration/policy-engine/http/rest/dto/evaluation.dto' import { SignMessageRequestDto } from '@app/orchestration/policy-engine/http/rest/dto/sign-message-request.dto' import { SignTransactionRequestDto } from '@app/orchestration/policy-engine/http/rest/dto/sign-transaction-request.dto' +import { SignatureDto } from '@app/orchestration/policy-engine/http/rest/dto/signature.dto' +import { TransactionResponseDto } from '@app/orchestration/policy-engine/http/rest/dto/transaction-request.dto' import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger' -import { Transform, Type } from 'class-transformer' -import { IsOptional, IsString } from 'class-validator' +import { Type } from 'class-transformer' +import { IsDefined, IsString, ValidateNested } from 'class-validator' -/** - * The transformer function in the "@Transformer" decorator for bigint - * properties differs between the request and response. This variation is due to - * the limitations of JS' built-in functions, such as JSON, when handling - * bigints. - * - * - Request: The transformer converts from a string to bigint. - * - Response: The transformer converts from bigint to a string. - */ class SignTransactionResponseDto extends SignTransactionRequestDto { - @IsString() - @IsOptional() - @Transform(({ value }) => value.toString()) + // Use a different DTO for the response to ensure the conversion of attributes + // using bigint back to string. + @IsDefined() + @ValidateNested() + // IMPORTANT: the redundant @Type decorator call with the same DTO from the + // type is to ensure nested serialization. + @Type(() => TransactionResponseDto) @ApiProperty({ - type: 'string' + type: TransactionResponseDto }) - gas?: bigint + transactionRequest: TransactionResponseDto } -// Just for keeping consistency on the naming. +// Nothing different, just keeping naming consistency. class SignMessageResponseDto extends SignMessageRequestDto {} +// TODO (@wcalderipe, 22/01/24): Missing the authentication attribute. @ApiExtraModels(SignTransactionResponseDto, SignMessageResponseDto) export class AuthorizationResponseDto { @ApiProperty() @@ -36,9 +34,6 @@ export class AuthorizationResponseDto { @ApiProperty() orgId: string - @ApiProperty() - initiatorId: string - @IsString() @ApiProperty({ required: false, @@ -47,29 +42,30 @@ export class AuthorizationResponseDto { }) idempotencyKey?: string | null - @ApiProperty({ - enum: SupportedAction - }) - action: `${SupportedAction}` + @IsDefined() + @ValidateNested() + @ApiProperty() + @Type(() => SignatureDto) + authentication: SignatureDto @ApiProperty({ enum: AuthorizationRequestStatus }) status: `${AuthorizationRequestStatus}` + @Type(() => EvaluationDto) @ApiProperty({ - description: 'The hash of the request in EIP-191 format.' - }) - hash: string - - @ApiProperty({ - type: [EvaluationDto] + type: EvaluationDto, + isArray: true }) - @Type(() => EvaluationDto) evaluations: EvaluationDto[] + // TODO (@wcalderipe, 22/01/24): Test the discrimination type option from + // class-transformer instead of a custom function map. + // + // See https://github.com/typestack/class-transformer?tab=readme-ov-file#working-with-nested-objects @Type((opts) => { - return opts?.object.action === SupportedAction.SIGN_TRANSACTION + return opts?.object.request.action === SupportedAction.SIGN_TRANSACTION ? SignTransactionResponseDto : SignMessageResponseDto }) diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/sign-message-request.dto.ts b/apps/orchestration/src/policy-engine/http/rest/dto/sign-message-request.dto.ts index 7f3a59f15..7dc33e693 100644 --- a/apps/orchestration/src/policy-engine/http/rest/dto/sign-message-request.dto.ts +++ b/apps/orchestration/src/policy-engine/http/rest/dto/sign-message-request.dto.ts @@ -1,12 +1,28 @@ +import { SupportedAction } from '@app/orchestration/policy-engine/core/type/domain.type' import { ApiProperty } from '@nestjs/swagger' -import { IsDefined, IsString } from 'class-validator' +import { IsDefined, IsEnum, IsString } from 'class-validator' export class SignMessageRequestDto { - @IsString() + @IsEnum(SupportedAction) @IsDefined() @ApiProperty({ - required: true, - type: 'string' + enum: SupportedAction, + default: SupportedAction.SIGN_MESSAGE }) + action: `${SupportedAction.SIGN_MESSAGE}` + + @IsString() + @IsDefined() + @ApiProperty() + nonce: string + + @IsString() + @IsDefined() + @ApiProperty() + resourceId: string + + @IsString() + @IsDefined() + @ApiProperty() message: string } diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/sign-transaction-request.dto.ts b/apps/orchestration/src/policy-engine/http/rest/dto/sign-transaction-request.dto.ts index 658afaf68..6bc6a7a32 100644 --- a/apps/orchestration/src/policy-engine/http/rest/dto/sign-transaction-request.dto.ts +++ b/apps/orchestration/src/policy-engine/http/rest/dto/sign-transaction-request.dto.ts @@ -1,120 +1,31 @@ -import { Address, Hex } from '@narval/authz-shared' +import { SupportedAction } from '@app/orchestration/policy-engine/core/type/domain.type' +import { TransactionRequestDto } from '@app/orchestration/policy-engine/http/rest/dto/transaction-request.dto' import { ApiProperty } from '@nestjs/swagger' -import { Transform } from 'class-transformer' -import { - IsDefined, - IsEthereumAddress, - IsInt, - IsNumber, - IsOptional, - IsString, - Min, - ValidateNested -} from 'class-validator' +import { IsDefined, IsEnum, IsString, ValidateNested } from 'class-validator' -class AccessListDto { - @IsString() +export class SignTransactionRequestDto { + @IsEnum(SupportedAction) @IsDefined() - @IsEthereumAddress() - @Transform(({ value }) => value.toLowerCase()) @ApiProperty({ - format: 'Address', - required: true, - type: 'string' + enum: SupportedAction, + default: SupportedAction.SIGN_TRANSACTION }) - address: Address + action: `${SupportedAction.SIGN_TRANSACTION}` @IsString() - @ApiProperty({ - format: 'Hexadecimal', - isArray: true, - required: true, - type: 'string' - }) - storageKeys: Hex[] -} - -export class SignTransactionRequestDto { - @IsInt() - @Min(1) - @ApiProperty({ - minimum: 1 - }) - chainId: number + @IsDefined() + @ApiProperty() + nonce: string @IsString() @IsDefined() - @IsEthereumAddress() - @Transform(({ value }) => value.toLowerCase()) - @ApiProperty({ - format: 'address', - type: 'string' - }) - from: Address + @ApiProperty() + resourceId: string - // @TODO (@wcalderipe, 19/01/24): Are we accepting the transaction without a - // nonce? - @IsNumber() - @IsOptional() - @Min(0) - @ApiProperty({ - minimum: 0, - required: false - }) - nonce?: number - - @IsOptional() + @IsDefined() @ValidateNested() @ApiProperty({ - isArray: true, - required: false, - type: AccessListDto - }) - accessList?: AccessListDto[] - - @IsString() - @IsOptional() - @ApiProperty({ - format: 'hexadecimal', - required: false, - type: 'string' - }) - data?: Hex - - @IsOptional() - @Transform(({ value }) => BigInt(value)) - @ApiProperty({ - format: 'bigint', - required: false, - type: 'string' - }) - gas?: bigint - - @IsString() - @IsEthereumAddress() - @IsOptional() - @Transform(({ value }) => value.toLowerCase()) - @ApiProperty({ - format: 'address', - required: false, - type: 'string' - }) - to?: Address | null - - @IsString() - @IsOptional() - @ApiProperty({ - default: '2', - required: false - }) - type?: '2' - - @IsString() - @IsOptional() - @ApiProperty({ - format: 'hexadecimal', - required: false, - type: 'string' + type: TransactionRequestDto }) - value?: Hex + transactionRequest: TransactionRequestDto } diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/signature.dto.ts b/apps/orchestration/src/policy-engine/http/rest/dto/signature.dto.ts index 46b744f07..6f6850ab5 100644 --- a/apps/orchestration/src/policy-engine/http/rest/dto/signature.dto.ts +++ b/apps/orchestration/src/policy-engine/http/rest/dto/signature.dto.ts @@ -4,12 +4,26 @@ import { IsDefined, IsOptional, IsString } from 'class-validator' export class SignatureDto { @IsString() @IsDefined() - @ApiProperty() + // TODO (@wcalderipe, 23/01/24): Coerce to lowercase once the AuthZ accepts + // case-insensitive. + // See https://linear.app/narval/issue/NAR-1531 + // + // @Transform(({ value }) => value.toLowerCase()) + @ApiProperty({ + format: 'lowercase' + }) sig: string @IsString() @IsDefined() - @ApiProperty() + // TODO (@wcalderipe, 23/01/24): Coerce to lowercase once the AuthZ accepts + // case-insensitive. + // See https://linear.app/narval/issue/NAR-1531 + // + // @Transform(({ value }) => value.toLowerCase()) + @ApiProperty({ + format: 'lowercase' + }) pubKey: string @IsString() diff --git a/apps/orchestration/src/policy-engine/http/rest/dto/transaction-request.dto.ts b/apps/orchestration/src/policy-engine/http/rest/dto/transaction-request.dto.ts new file mode 100644 index 000000000..e7d06337b --- /dev/null +++ b/apps/orchestration/src/policy-engine/http/rest/dto/transaction-request.dto.ts @@ -0,0 +1,137 @@ +import { Address, Hex } from '@narval/authz-shared' +import { ApiProperty } from '@nestjs/swagger' +import { Transform } from 'class-transformer' +import { + IsDefined, + IsEthereumAddress, + IsInt, + IsNumber, + IsOptional, + IsString, + Min, + ValidateNested +} from 'class-validator' + +class AccessListDto { + @IsString() + @IsDefined() + @IsEthereumAddress() + @Transform(({ value }) => value.toLowerCase()) + @ApiProperty({ + format: 'address', + required: true, + type: 'string' + }) + address: Address + + @IsString() + @ApiProperty({ + format: 'hexadecimal', + isArray: true, + required: true, + type: 'string' + }) + storageKeys: Hex[] +} + +export class TransactionRequestDto { + @IsInt() + @Min(1) + @ApiProperty({ + minimum: 1 + }) + chainId: number + + @IsString() + @IsDefined() + @IsEthereumAddress() + @Transform(({ value }) => value.toLowerCase()) + @ApiProperty({ + format: 'address', + type: 'string' + }) + from: Address + + @IsNumber() + @IsOptional() + @Min(0) + @ApiProperty({ + minimum: 0, + required: false + }) + nonce?: number + + @IsOptional() + @ValidateNested() + @ApiProperty({ + isArray: true, + required: false, + type: AccessListDto + }) + accessList?: AccessListDto[] + + @IsString() + @IsOptional() + @ApiProperty({ + format: 'hexadecimal', + required: false, + type: 'string' + }) + data?: Hex + + @IsOptional() + @Transform(({ value }) => BigInt(value)) + @ApiProperty({ + format: 'bigint', + required: false, + type: 'string' + }) + gas?: bigint + + @IsString() + @IsEthereumAddress() + @IsOptional() + @Transform(({ value }) => value.toLowerCase()) + @ApiProperty({ + format: 'address', + required: false, + type: 'string' + }) + to?: Address | null + + @IsString() + @IsOptional() + @ApiProperty({ + default: '2', + required: false + }) + type?: '2' + + @IsString() + @IsOptional() + @ApiProperty({ + format: 'hexadecimal', + required: false, + type: 'string' + }) + value?: Hex +} + +/** + * The transformer function in the "@Transformer" decorator for bigint + * properties differs between the request and response. This variation is due to + * the limitations of JS' built-in functions, such as JSON, when handling + * bigints. + * + * - Request: The transformer converts from a string to bigint. + * - Response: The transformer converts from bigint to a string. + */ +export class TransactionResponseDto extends TransactionRequestDto { + @IsString() + @IsOptional() + @Transform(({ value }) => value.toString()) + @ApiProperty({ + type: 'string' + }) + gas?: bigint +} diff --git a/apps/orchestration/src/policy-engine/http/rest/util.ts b/apps/orchestration/src/policy-engine/http/rest/util.ts index 0a53bba62..057494a23 100644 --- a/apps/orchestration/src/policy-engine/http/rest/util.ts +++ b/apps/orchestration/src/policy-engine/http/rest/util.ts @@ -1,8 +1,7 @@ import { CreateApproval, CreateAuthorizationRequest, - Signature, - SupportedAction + Signature } from '@app/orchestration/policy-engine/core/type/domain.type' import { AuthorizationRequestDto } from '@app/orchestration/policy-engine/http/rest/dto/authorization-request.dto' import { plainToInstance } from 'class-transformer' @@ -18,37 +17,11 @@ export const toCreateAuthorizationRequest = ( const approvals: CreateApproval[] = dto.approvals const authentication: Signature = dto.authentication - const shared = { + return { orgId, approvals, authentication, - hash: dto.hash, - evaluations: [] - } - - if (dto.isSignMessage(dto.request)) { - return { - ...shared, - action: SupportedAction.SIGN_MESSAGE, - request: { - message: dto.request.message - } - } - } - - return { - ...shared, - action: SupportedAction.SIGN_TRANSACTION, - request: { - accessList: dto.request.accessList, - chainId: dto.request.chainId, - data: dto.request.data, - from: dto.request.from, - gas: dto.request.gas, - nonce: dto.request.nonce, - to: dto.request.to, - type: dto.request.type, - value: dto.request.value - } + evaluations: [], + request: body.request } } diff --git a/apps/orchestration/src/policy-engine/persistence/decode/__test__/unit/authorization-request.decode.spec.ts b/apps/orchestration/src/policy-engine/persistence/decode/__test__/unit/authorization-request.decode.spec.ts index 56ffb247f..5ebb93b72 100644 --- a/apps/orchestration/src/policy-engine/persistence/decode/__test__/unit/authorization-request.decode.spec.ts +++ b/apps/orchestration/src/policy-engine/persistence/decode/__test__/unit/authorization-request.decode.spec.ts @@ -1,14 +1,14 @@ import { SupportedAction } from '@app/orchestration/policy-engine/core/type/domain.type' import { decodeAuthorizationRequest } from '@app/orchestration/policy-engine/persistence/decode/authorization-request.decode' import { DecodeAuthorizationRequestException } from '@app/orchestration/policy-engine/persistence/exception/decode-authorization-request.exception' -import { AuthorizationRequestAction, AuthorizationRequestStatus } from '@prisma/client/orchestration' +import { AuthorizationRequestModel } from '@app/orchestration/policy-engine/persistence/type/model.type' +import { AuthorizationRequestStatus } from '@prisma/client/orchestration' describe('decodeAuthorizationRequest', () => { - const sharedModel = { + const sharedModel: Omit = { id: '3356d68c-bc63-4b08-9253-289eec475d1d', orgId: 'f6477ee7-7f5e-4e19-92f9-7864c7af5fd4', status: AuthorizationRequestStatus.CREATED, - hash: 'test-request-hash', idempotencyKey: null, authnSig: '0xe24d097cea880a40f8be2cf42f497b9fbda5f9e4a31b596827e051d78dce75c032fa7e5ee3046f7c6f116e5b98cb8d268fa9b9d222ff44719e2ec2a0d9159d0d1c', @@ -26,14 +26,17 @@ describe('decodeAuthorizationRequest', () => { ...sharedModel, action: SupportedAction.SIGN_TRANSACTION, request: { - from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', - chainId: 1, - nonce: 1 + action: SupportedAction.SIGN_TRANSACTION, + nonce: '99', + resourceId: '440b486a-8807-49d8-97a1-24c2920730ed', + transactionRequest: { + from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', + chainId: 1, + nonce: 1 + } } } - AuthorizationRequestAction - expect(() => { decodeAuthorizationRequest(validModel) }).not.toThrow(DecodeAuthorizationRequestException) @@ -44,8 +47,13 @@ describe('decodeAuthorizationRequest', () => { ...sharedModel, action: SupportedAction.SIGN_TRANSACTION, request: { - from: 'not-an-ethereum-address', - gas: '5000' + action: SupportedAction.SIGN_TRANSACTION, + nonce: '99', + resourceId: '440b486a-8807-49d8-97a1-24c2920730ed', + transactionRequest: { + from: 'not-an-ethereum-address', + gas: '5000' + } } } @@ -53,27 +61,6 @@ describe('decodeAuthorizationRequest', () => { decodeAuthorizationRequest(invalidModel) }).toThrow(DecodeAuthorizationRequestException) }) - - it.skip('throws DecodeAuthorizationRequestException when null/undefined coerces to bigint error', () => { - const requestWithGasNull = { - from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', - to: '0xbbb7be636c3ad8cf9d08ba8bdba4abd2ef29bd23', - data: '0x', - gas: null - } - const model = { - ...sharedModel, - action: SupportedAction.SIGN_TRANSACTION - } - - expect(() => { - decodeAuthorizationRequest({ ...model, request: requestWithGasNull }) - }).toThrow(DecodeAuthorizationRequestException) - - expect(() => { - decodeAuthorizationRequest({ ...model, request: { ...requestWithGasNull, gas: undefined } }) - }).toThrow(DecodeAuthorizationRequestException) - }) }) describe('sign message', () => { @@ -82,6 +69,9 @@ describe('decodeAuthorizationRequest', () => { ...sharedModel, action: SupportedAction.SIGN_MESSAGE, request: { + action: SupportedAction.SIGN_MESSAGE, + nonce: '99', + resourceId: '440b486a-8807-49d8-97a1-24c2920730ed', message: 'Test messsage' } } diff --git a/apps/orchestration/src/policy-engine/persistence/decode/authorization-request.decode.ts b/apps/orchestration/src/policy-engine/persistence/decode/authorization-request.decode.ts index 2b24c185e..bf9a51389 100644 --- a/apps/orchestration/src/policy-engine/persistence/decode/authorization-request.decode.ts +++ b/apps/orchestration/src/policy-engine/persistence/decode/authorization-request.decode.ts @@ -1,15 +1,13 @@ import { AuthorizationRequest, Evaluation } from '@app/orchestration/policy-engine/core/type/domain.type' import { DecodeAuthorizationRequestException } from '@app/orchestration/policy-engine/persistence/exception/decode-authorization-request.exception' +import { AuthorizationRequestModel } from '@app/orchestration/policy-engine/persistence/type/model.type' import { ACTION_REQUEST } from '@app/orchestration/policy-engine/policy-engine.constant' -import { - AuthorizationRequestApproval, - AuthorizationRequest as AuthorizationRequestModel, - EvaluationLog -} from '@prisma/client/orchestration' +import { EvaluationLog } from '@prisma/client/orchestration' import { omit } from 'lodash/fp' +import { SetOptional } from 'type-fest' import { ZodIssueCode, ZodSchema } from 'zod' -type Model = AuthorizationRequestModel & { evaluationLog?: EvaluationLog[]; approvals: AuthorizationRequestApproval[] } +type Model = SetOptional const buildEvaluation = ({ id, decision, signature, createdAt }: EvaluationLog): Evaluation => ({ id, @@ -22,7 +20,6 @@ const buildSharedAttributes = (model: Model): Omit { pubKey: '0xd75D626a116D4a1959fE3bB938B2e7c116A05890' } - const signMessageRequest: SignMessageAuthorizationRequest = { + const signMessageRequest: AuthorizationRequest = { authentication, id: '6c7e92fc-d2b0-4840-8e9b-485393ecdf89', orgId: org.id, status: AuthorizationRequestStatus.PROCESSING, - action: SupportedAction.SIGN_MESSAGE, request: { + action: SupportedAction.SIGN_MESSAGE, + nonce: '99', + resourceId: '239bb48b-f708-47ba-97fa-ef336be4dffe', message: 'Test request' }, - hash: 'test-hash', idempotencyKey: null, approvals: [], evaluations: [], @@ -161,10 +161,11 @@ describe(AuthorizationRequestRepository.name, () => { }) describe(`when action is ${SupportedAction.SIGN_TRANSACTION}`, () => { - const signTransactionRequest: SignTransactionAuthorizationRequest = { - ...signMessageRequest, + const signTransaction: SignTransaction = { action: SupportedAction.SIGN_TRANSACTION, - request: { + nonce: '99', + resourceId: '3be0c61d-9b41-423f-80b8-ea6f7624d917', + transactionRequest: { from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', gas: BigInt(5_000), chainId: 1, @@ -172,6 +173,11 @@ describe(AuthorizationRequestRepository.name, () => { } } + const signTransactionRequest: AuthorizationRequest = { + ...signMessageRequest, + request: signTransaction + } + it('encodes bigints as strings', async () => { await repository.create(signTransactionRequest) @@ -179,8 +185,8 @@ describe(AuthorizationRequestRepository.name, () => { expect(authzRequest).not.toEqual(null) - if (authzRequest && isSignTransaction(authzRequest)) { - expect(authzRequest?.request.gas).toEqual(signTransactionRequest.request.gas) + if (authzRequest && authzRequest.request.action === SupportedAction.SIGN_TRANSACTION) { + expect(authzRequest?.request.transactionRequest.gas).toEqual(signTransaction.transactionRequest.gas) } }) }) diff --git a/apps/orchestration/src/policy-engine/persistence/repository/authorization-request.repository.ts b/apps/orchestration/src/policy-engine/persistence/repository/authorization-request.repository.ts index 7842557f2..7ff8e9c40 100644 --- a/apps/orchestration/src/policy-engine/persistence/repository/authorization-request.repository.ts +++ b/apps/orchestration/src/policy-engine/persistence/repository/authorization-request.repository.ts @@ -5,10 +5,7 @@ import { Evaluation } from '@app/orchestration/policy-engine/core/type/domain.type' import { decodeAuthorizationRequest } from '@app/orchestration/policy-engine/persistence/decode/authorization-request.decode' -import { - createAuthorizationRequestSchema, - updateAuthorizationRequestSchema -} from '@app/orchestration/policy-engine/persistence/schema/authorization-request.schema' +import { createRequestSchema } from '@app/orchestration/policy-engine/persistence/schema/request.schema' import { PrismaService } from '@app/orchestration/shared/module/persistence/service/prisma.service' import { Injectable } from '@nestjs/common' import { EvaluationLog } from '@prisma/client/orchestration' @@ -19,20 +16,10 @@ export class AuthorizationRequestRepository { constructor(private prismaService: PrismaService) {} async create(input: CreateAuthorizationRequest): Promise { - const { - id, - action, - request, - orgId, - hash, - status, - idempotencyKey, - createdAt, - updatedAt, - evaluations, - approvals, - authentication - } = createAuthorizationRequestSchema.parse(this.getDefaults(input)) + const { id, orgId, status, idempotencyKey, createdAt, updatedAt, evaluations, approvals, authentication } = + this.getDefaults(input) + const request = createRequestSchema.parse(input.request) + const evaluationLogs = this.toEvaluationLogs(orgId, evaluations) const model = await this.prismaService.authorizationRequest.create({ @@ -40,12 +27,11 @@ export class AuthorizationRequestRepository { id, status, orgId, - action, request, - hash, idempotencyKey, createdAt, updatedAt, + action: request.action, authnAlg: authentication.alg, authnSig: authentication.sig, authnPubKey: authentication.pubKey, @@ -82,22 +68,24 @@ export class AuthorizationRequestRepository { input: Partial> & Pick ): Promise { - const { id } = input - const { orgId, status, evaluations, approvals } = updateAuthorizationRequestSchema.parse(input) + const { id, orgId, status, evaluations, approvals } = input const evaluationLogs = this.toEvaluationLogs(orgId, evaluations) + // TODO (@wcalderipe, 19/01/24): Cover the skipDuplicate with tests. const model = await this.prismaService.authorizationRequest.update({ where: { id }, data: { status, approvals: { createMany: { - data: approvals?.length ? approvals : [] + data: approvals?.length ? approvals : [], + skipDuplicates: true } }, evaluationLog: { createMany: { - data: evaluationLogs + data: evaluationLogs, + skipDuplicates: true } } }, diff --git a/apps/orchestration/src/policy-engine/persistence/schema/__test__/unit/sign-transaction-request.schema.spec.ts b/apps/orchestration/src/policy-engine/persistence/schema/__test__/unit/sign-transaction-request.schema.spec.ts deleted file mode 100644 index 5d64782a4..000000000 --- a/apps/orchestration/src/policy-engine/persistence/schema/__test__/unit/sign-transaction-request.schema.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { readSignTransactionRequestSchema } from '@app/orchestration/policy-engine/persistence/schema/sign-transaction-request.schema' -import { z } from 'zod' - -type ReadSignTransactionRequest = z.infer - -describe('signTransactionRequestSchema', () => { - describe('read', () => { - it('parses a sign transaction request', () => { - const signTransactionRequest: ReadSignTransactionRequest = { - from: '0xaaa8ee1cbaa1856f4550c6fc24abb16c5c9b2a43', - to: '0xbbb7be636c3ad8cf9d08ba8bdba4abd2ef29bd23', - data: '0x', - gas: BigInt(5_000), - chainId: 1, - nonce: 0 - } - - const parse = readSignTransactionRequestSchema.safeParse(signTransactionRequest) - - expect(parse.success).toEqual(true) - }) - }) -}) diff --git a/apps/orchestration/src/policy-engine/persistence/schema/authorization-request.schema.ts b/apps/orchestration/src/policy-engine/persistence/schema/authorization-request.schema.ts deleted file mode 100644 index 465407fae..000000000 --- a/apps/orchestration/src/policy-engine/persistence/schema/authorization-request.schema.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { SupportedAction } from '@app/orchestration/policy-engine/core/type/domain.type' -import { - createSignMessageRequestSchema, - readSignMessageRequestSchema -} from '@app/orchestration/policy-engine/persistence/schema/sign-message-request.schema' -import { - createSignTransactionRequestSchema, - readSignTransactionRequestSchema -} from '@app/orchestration/policy-engine/persistence/schema/sign-transaction-request.schema' -import { AuthorizationRequestStatus } from '@prisma/client/orchestration' -import { z } from 'zod' - -const signatureSchema = z.object({ - sig: z.string(), - alg: z.string(), - pubKey: z.string() -}) - -const approvalSchema = signatureSchema.extend({ - id: z.string().uuid(), - createdAt: z.date() -}) - -const evaluationSchema = z.object({ - id: z.string().uuid(), - decision: z.string(), - signature: z.string().nullable(), - createdAt: z.date() -}) - -const sharedAuthorizationRequestSchema = z.object({ - id: z.string().uuid(), - orgId: z.string().uuid(), - status: z.nativeEnum(AuthorizationRequestStatus), - hash: z.string(), - authentication: signatureSchema, - idempotencyKey: z.string().nullish(), - approvals: z.array(approvalSchema), - evaluations: z.array(evaluationSchema), - createdAt: z.date(), - updatedAt: z.date() -}) - -export const readAuthorizationRequestSchema = z.discriminatedUnion('action', [ - sharedAuthorizationRequestSchema.extend({ - action: z.literal(SupportedAction.SIGN_MESSAGE), - request: readSignMessageRequestSchema - }), - sharedAuthorizationRequestSchema.extend({ - action: z.literal(SupportedAction.SIGN_TRANSACTION), - request: readSignTransactionRequestSchema - }) -]) - -const createSharedAuthorizationRequestSchema = sharedAuthorizationRequestSchema.partial({ - status: true -}) - -export const createAuthorizationRequestSchema = z.discriminatedUnion('action', [ - createSharedAuthorizationRequestSchema.extend({ - action: z.literal(SupportedAction.SIGN_MESSAGE), - request: createSignMessageRequestSchema - }), - createSharedAuthorizationRequestSchema.extend({ - action: z.literal(SupportedAction.SIGN_TRANSACTION), - request: createSignTransactionRequestSchema - }) -]) - -/** - * Only allow updating a few attributes of the authorization request. - * - * This restriction is in place because altering the data of an authorization - * request would mean tampering with the user's original request. - */ -export const updateAuthorizationRequestSchema = sharedAuthorizationRequestSchema - .pick({ - id: true, - orgId: true, - status: true, - // The update operation creates evaluations and approvals in the - // authorization request. - evaluations: true, - approvals: true - }) - .partial() diff --git a/apps/orchestration/src/policy-engine/persistence/schema/request.schema.ts b/apps/orchestration/src/policy-engine/persistence/schema/request.schema.ts new file mode 100644 index 000000000..b84a64fb5 --- /dev/null +++ b/apps/orchestration/src/policy-engine/persistence/schema/request.schema.ts @@ -0,0 +1,16 @@ +import { + createSignMessageSchema, + readSignMessageSchema +} from '@app/orchestration/policy-engine/persistence/schema/sign-message.schema' +import { + createSignTransactionSchema, + readSignTransactionSchema +} from '@app/orchestration/policy-engine/persistence/schema/sign-transaction.schema' +import { z } from 'zod' + +export const readRequestSchema = z.discriminatedUnion('action', [readSignTransactionSchema, readSignMessageSchema]) + +export const createRequestSchema = z.discriminatedUnion('action', [ + createSignTransactionSchema, + createSignMessageSchema +]) diff --git a/apps/orchestration/src/policy-engine/persistence/schema/sign-message-request.schema.ts b/apps/orchestration/src/policy-engine/persistence/schema/sign-message-request.schema.ts deleted file mode 100644 index 329e44ce9..000000000 --- a/apps/orchestration/src/policy-engine/persistence/schema/sign-message-request.schema.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from 'zod' - -export const readSignMessageRequestSchema = z.object({ - message: z.string() -}) - -export const createSignMessageRequestSchema = readSignMessageRequestSchema diff --git a/apps/orchestration/src/policy-engine/persistence/schema/sign-message.schema.ts b/apps/orchestration/src/policy-engine/persistence/schema/sign-message.schema.ts new file mode 100644 index 000000000..d4172e5a5 --- /dev/null +++ b/apps/orchestration/src/policy-engine/persistence/schema/sign-message.schema.ts @@ -0,0 +1,11 @@ +import { SupportedAction } from '@app/orchestration/policy-engine/core/type/domain.type' +import { z } from 'zod' + +export const readSignMessageSchema = z.object({ + action: z.literal(SupportedAction.SIGN_MESSAGE), + nonce: z.string(), + resourceId: z.string(), + message: z.string() +}) + +export const createSignMessageSchema = readSignMessageSchema diff --git a/apps/orchestration/src/policy-engine/persistence/schema/sign-transaction.schema.ts b/apps/orchestration/src/policy-engine/persistence/schema/sign-transaction.schema.ts new file mode 100644 index 000000000..518944557 --- /dev/null +++ b/apps/orchestration/src/policy-engine/persistence/schema/sign-transaction.schema.ts @@ -0,0 +1,17 @@ +import { SupportedAction } from '@app/orchestration/policy-engine/core/type/domain.type' +import { + createTransactionRequestSchema, + readTransactionRequestSchema +} from '@app/orchestration/policy-engine/persistence/schema/transaction-request.schema' +import { z } from 'zod' + +export const readSignTransactionSchema = z.object({ + action: z.literal(SupportedAction.SIGN_TRANSACTION), + nonce: z.string(), + resourceId: z.string(), + transactionRequest: readTransactionRequestSchema +}) + +export const createSignTransactionSchema = readSignTransactionSchema.extend({ + transactionRequest: createTransactionRequestSchema +}) diff --git a/apps/orchestration/src/policy-engine/persistence/schema/sign-transaction-request.schema.ts b/apps/orchestration/src/policy-engine/persistence/schema/transaction-request.schema.ts similarity index 82% rename from apps/orchestration/src/policy-engine/persistence/schema/sign-transaction-request.schema.ts rename to apps/orchestration/src/policy-engine/persistence/schema/transaction-request.schema.ts index a84259f14..dd304674c 100644 --- a/apps/orchestration/src/policy-engine/persistence/schema/sign-transaction-request.schema.ts +++ b/apps/orchestration/src/policy-engine/persistence/schema/transaction-request.schema.ts @@ -1,4 +1,3 @@ -import { TransactionType } from '@narval/authz-shared' import { isAddress, isHex } from 'viem' import { z } from 'zod' @@ -47,7 +46,7 @@ const accessListSchema = z.object({ storageKeys: z.array(hexSchema) }) -export const readSignTransactionRequestSchema = z.object({ +export const readTransactionRequestSchema = z.object({ chainId: z.coerce.number(), from: addressSchema, nonce: z.coerce.number().optional(), @@ -55,10 +54,10 @@ export const readSignTransactionRequestSchema = z.object({ data: hexSchema.optional(), gas: z.coerce.bigint().optional(), to: addressSchema.optional(), - type: z.nativeEnum(TransactionType).optional(), + type: z.literal('2').optional(), value: hexSchema.optional() }) -export const createSignTransactionRequestSchema = readSignTransactionRequestSchema.extend({ +export const createTransactionRequestSchema = readTransactionRequestSchema.extend({ gas: z.coerce.string().optional() }) diff --git a/apps/orchestration/src/policy-engine/persistence/type/model.type.ts b/apps/orchestration/src/policy-engine/persistence/type/model.type.ts new file mode 100644 index 000000000..8a337af5a --- /dev/null +++ b/apps/orchestration/src/policy-engine/persistence/type/model.type.ts @@ -0,0 +1,6 @@ +import { AuthorizationRequest, AuthorizationRequestApproval, EvaluationLog } from '@prisma/client/orchestration' + +export type AuthorizationRequestModel = AuthorizationRequest & { + evaluationLog: EvaluationLog[] + approvals: AuthorizationRequestApproval[] +} diff --git a/apps/orchestration/src/policy-engine/policy-engine.constant.ts b/apps/orchestration/src/policy-engine/policy-engine.constant.ts index ad1f39d80..495c8539f 100644 --- a/apps/orchestration/src/policy-engine/policy-engine.constant.ts +++ b/apps/orchestration/src/policy-engine/policy-engine.constant.ts @@ -1,12 +1,12 @@ import { SupportedAction } from '@app/orchestration/policy-engine/core/type/domain.type' import { - createSignMessageRequestSchema, - readSignMessageRequestSchema -} from '@app/orchestration/policy-engine/persistence/schema/sign-message-request.schema' + createSignMessageSchema, + readSignMessageSchema +} from '@app/orchestration/policy-engine/persistence/schema/sign-message.schema' import { - createSignTransactionRequestSchema, - readSignTransactionRequestSchema -} from '@app/orchestration/policy-engine/persistence/schema/sign-transaction-request.schema' + createSignTransactionSchema, + readSignTransactionSchema +} from '@app/orchestration/policy-engine/persistence/schema/sign-transaction.schema' import { ZodType } from 'zod' type ActionRequestConfig = { @@ -21,15 +21,15 @@ export const ACTION_REQUEST: Record = { [SupportedAction.SIGN_MESSAGE]: { action: SupportedAction.SIGN_MESSAGE, schema: { - read: readSignMessageRequestSchema, - create: createSignMessageRequestSchema + read: readSignMessageSchema, + create: createSignMessageSchema } }, [SupportedAction.SIGN_TRANSACTION]: { action: SupportedAction.SIGN_TRANSACTION, schema: { - read: readSignTransactionRequestSchema, - create: createSignTransactionRequestSchema + read: readSignTransactionSchema, + create: createSignTransactionSchema } } } diff --git a/apps/orchestration/src/policy-engine/policy-engine.module.ts b/apps/orchestration/src/policy-engine/policy-engine.module.ts index e5c440fb5..ec718ea4f 100644 --- a/apps/orchestration/src/policy-engine/policy-engine.module.ts +++ b/apps/orchestration/src/policy-engine/policy-engine.module.ts @@ -20,7 +20,7 @@ import { AuthorizationRequestProcessingProducer } from './queue/producer/authori ConfigModule.forRoot(), HttpModule, PersistenceModule, - // TODO (@wcalderipe, 11/01/24): Figure out why can I have a wrapper to + // TODO (@wcalderipe, 11/01/24): Figure out why can't I have a wrapper to // register both queue and board at the same time. // // Desired DevX: QueueModule.registerQueue({ name: "my-queue" }) diff --git a/apps/orchestration/src/policy-engine/queue/consumer/__test__/integration/authorization-request-processing.consumer.spec.ts b/apps/orchestration/src/policy-engine/queue/consumer/__test__/integration/authorization-request-processing.consumer.spec.ts index a4f5b83d9..d29d203fb 100644 --- a/apps/orchestration/src/policy-engine/queue/consumer/__test__/integration/authorization-request-processing.consumer.spec.ts +++ b/apps/orchestration/src/policy-engine/queue/consumer/__test__/integration/authorization-request-processing.consumer.spec.ts @@ -51,11 +51,12 @@ describe(AuthorizationRequestProcessingConsumer.name, () => { id: '6c7e92fc-d2b0-4840-8e9b-485393ecdf89', orgId: org.id, status: AuthorizationRequestStatus.PROCESSING, - action: SupportedAction.SIGN_MESSAGE, request: { + action: SupportedAction.SIGN_MESSAGE, + nonce: '99', + resourceId: '239bb48b-f708-47ba-97fa-ef336be4dffe', message: 'Test request' }, - hash: 'test-hash', idempotencyKey: null, evaluations: [], approvals: [], diff --git a/apps/orchestration/src/policy-engine/queue/producer/__test__/integration/authorization-request-processing.producer.spec.ts b/apps/orchestration/src/policy-engine/queue/producer/__test__/integration/authorization-request-processing.producer.spec.ts index fcf7737ea..0bc4237ff 100644 --- a/apps/orchestration/src/policy-engine/queue/producer/__test__/integration/authorization-request-processing.producer.spec.ts +++ b/apps/orchestration/src/policy-engine/queue/producer/__test__/integration/authorization-request-processing.producer.spec.ts @@ -37,11 +37,12 @@ describe(AuthorizationRequestProcessingProducer.name, () => { id: '6c7e92fc-d2b0-4840-8e9b-485393ecdf89', orgId: 'ac1374c2-fd62-4b6e-bd49-a4afcdcb91cc', status: AuthorizationRequestStatus.CREATED, - action: SupportedAction.SIGN_MESSAGE, request: { + action: SupportedAction.SIGN_MESSAGE, + nonce: '99', + resourceId: '15d13f33-b7fb-4b96-b8c2-f35c6b2f64dd', message: 'Test request' }, - hash: 'test-hash', idempotencyKey: null, evaluations: [], approvals: [], diff --git a/apps/orchestration/src/repl.ts b/apps/orchestration/src/repl.ts new file mode 100644 index 000000000..4d4248cb1 --- /dev/null +++ b/apps/orchestration/src/repl.ts @@ -0,0 +1,13 @@ +import { OrchestrationModule } from '@app/orchestration/orchestration.module' +import { repl } from '@nestjs/core' + +async function bootstrap() { + try { + await repl(OrchestrationModule) + } catch (error) { + console.log('### ERROR', error) + throw error + } +} + +bootstrap() diff --git a/apps/orchestration/src/shared/module/persistence/schema/migrations/20240119130400_init/migration.sql b/apps/orchestration/src/shared/module/persistence/schema/migrations/20240122174726_init/migration.sql similarity index 98% rename from apps/orchestration/src/shared/module/persistence/schema/migrations/20240119130400_init/migration.sql rename to apps/orchestration/src/shared/module/persistence/schema/migrations/20240122174726_init/migration.sql index 0d70889b7..76c8f2d02 100644 --- a/apps/orchestration/src/shared/module/persistence/schema/migrations/20240119130400_init/migration.sql +++ b/apps/orchestration/src/shared/module/persistence/schema/migrations/20240122174726_init/migration.sql @@ -20,7 +20,6 @@ CREATE TABLE "authorization_request" ( "org_id" TEXT NOT NULL, "status" "AuthorizationRequestStatus" NOT NULL DEFAULT 'CREATED', "action" "AuthorizationRequestAction" NOT NULL, - "hash" TEXT NOT NULL, "request" JSONB NOT NULL, "idempotency_key" TEXT, "authn_sig" TEXT NOT NULL, diff --git a/apps/orchestration/src/shared/module/persistence/schema/schema.prisma b/apps/orchestration/src/shared/module/persistence/schema/schema.prisma index 89da30b39..ef1525008 100644 --- a/apps/orchestration/src/shared/module/persistence/schema/schema.prisma +++ b/apps/orchestration/src/shared/module/persistence/schema/schema.prisma @@ -40,8 +40,8 @@ model AuthorizationRequest { id String @id @default(uuid()) @db.VarChar(255) orgId String @map("org_id") status AuthorizationRequestStatus @default(CREATED) + // Duplicate the action to pick the `request` schema at the application-level. action AuthorizationRequestAction - hash String request Json idempotencyKey String? @unique @map("idempotency_key") authnSig String @map("authn_sig") diff --git a/doc/idempotency-key-vs-nonce.md b/doc/idempotency-key-vs-nonce.md new file mode 100644 index 000000000..53ed8fcdf --- /dev/null +++ b/doc/idempotency-key-vs-nonce.md @@ -0,0 +1,66 @@ +# Idempotency Key vs Nonce — they are DIFFERENT concepts actually + +## TL;DR + +We need to use both nonce and idempotency key in our requests, because they serve +different purposes. + +The nonce is on the request that is being signed by the clients. This prevents +replay attacks ensuring each authorization can only be used once. +idempotency key is on the API request itself, used to handle errors & ensure we +don’t duplicate process. + +## Idempotency Key + +- Purpose: An idempotency key is used to ensure that a specific operation is + executed only once, even if the request is sent multiple times. It’s typically + used in scenarios where network uncertainty might cause a client to send the + same request multiple times (like processing payments). +- How It Works: The client generates a unique key and includes it with a + request. The server then checks if it has already processed a request with that + key. If it has, the server does not re-execute the operation but may return the + result of the original operation. +- Usage: Common in financial transactions, RESTful APIs, and any operation where + duplicate requests could lead to unintended consequences (like charging a credit + card multiple times). +- Example: A client submits a payment request with an idempotency key. If the + network fails and the client resends the request, the server recognizes the key + and doesn’t process the payment again. + +## Nonce + +- Purpose: A nonce (“number used once”) is a unique value that is used to ensure + freshness of a request. It’s primarily used to prevent replay attacks, where an + attacker could intercept a request and attempt to re-send it. +- How It Works: The server typically keeps track of nonces used in recent + requests. When a client sends a request with a nonce, the server checks if it + has seen that nonce before. If not, it processes the request; otherwise, it + rejects it. +- Usage: Common in cryptographic operations, authentication protocols, and APIs + where the timing and uniqueness of each request are critical for security. +- Example: In an authentication system, each login attempt might include a nonce + to ensure that an intercepted login request cannot be reused by an attacker. + +## Differences and when to use each + +- Uniqueness vs. Freshness: Idempotency keys ensure an operation is not + performed more than once, while nonces ensure a request is not processed more + than once by confirming its freshness. +- Scope: Idempotency keys are more about the business logic and operation level + (e.g., preventing duplicate transactions), whereas nonces are about the security + and protocol level (e.g., preventing replay attacks). +- Lifetime: Nonces generally have a short lifespan and are closely tied to the + timing of requests. Idempotency keys may have a longer lifespan, depending on + how long the server needs to recognize duplicate operations. + +## Conclusion + +- Use Idempotency Keys for operations where duplicate requests could lead to + unintended business consequences, like processing payments or creating + resources. +- Use Nonces for security-sensitive operations where you need to ensure that + each request is unique and cannot be replayed, like in authentication or + cryptographic signatures. + +In some cases, an API might use both mechanisms, leveraging idempotency keys for +operational integrity and nonces for security against replay attacks. diff --git a/package-lock.json b/package-lock.json index d0e43fed0..f1fc68e47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "clsx": "^1.2.1", + "date-fns": "^3.3.1", "lodash": "^4.17.21", "prism-react-renderer": "^2.3.1", "react": "18.2.0", @@ -48,7 +49,7 @@ "devDependencies": { "@automock/adapters.nestjs": "^2.1.0", "@automock/jest": "^2.1.0", - "@docusaurus/module-type-aliases": "3.1.0", + "@docusaurus/module-type-aliases": "3.0.0", "@docusaurus/tsconfig": "3.0.0", "@docusaurus/types": "3.1.0", "@golevelup/ts-jest": "^0.4.0", @@ -66,7 +67,7 @@ "@nx/workspace": "17.2.8", "@playwright/test": "^1.36.0", "@swc-node/register": "~1.6.7", - "@swc/core": "~1.3.85", + "@swc/core": "~1.3.105", "@testing-library/react": "14.1.2", "@types/jest": "^29.5.11", "@types/lodash": "^4.14.202", @@ -77,7 +78,7 @@ "@types/supertest": "^6.0.2", "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.18.0", - "@typescript-eslint/parser": "^6.9.1", + "@typescript-eslint/parser": "^6.19.1", "babel-jest": "^29.4.1", "dotenv-cli": "^7.3.0", "eslint": "~8.46.0", @@ -88,14 +89,16 @@ "eslint-plugin-playwright": "^0.21.0", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", + "husky": "8.0.3", "jest": "^29.4.1", "jest-environment-jsdom": "^29.4.1", "jest-environment-node": "^29.4.1", + "lint-staged": "15.2.0", "nock": "^13.4.0", "nx": "17.1.3", "prettier": "^2.6.2", "prettier-plugin-organize-imports": "^3.2.4", - "prisma": "^5.7.1", + "prisma": "^5.8.1", "supertest": "^6.3.4", "ts-jest": "^29.1.0", "ts-node": "10.9.1", @@ -3582,12 +3585,13 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.1.0.tgz", - "integrity": "sha512-XUl7Z4PWlKg4l6KF05JQ3iDHQxnPxbQUqTNKvviHyuHdlalOFv6qeDAm7IbzyQPJD5VA6y4dpRbTWSqP9ClwPg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.0.0.tgz", + "integrity": "sha512-CfC6CgN4u/ce+2+L1JdsHNyBd8yYjl4De2B2CBj2a9F7WuJ5RjV1ciuU7KDg8uyju+NRVllRgvJvxVUjCdkPiw==", + "dev": true, "dependencies": { "@docusaurus/react-loadable": "5.5.2", - "@docusaurus/types": "3.1.0", + "@docusaurus/types": "3.0.0", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -3600,6 +3604,35 @@ "react-dom": "*" } }, + "node_modules/@docusaurus/module-type-aliases/node_modules/@docusaurus/types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.0.0.tgz", + "integrity": "sha512-Qb+l/hmCOVemReuzvvcFdk84bUmUFyD0Zi81y651ie3VwMrXqC7C0E7yZLKMOsLj/vkqsxHbtkAuYMI89YzNzg==", + "dev": true, + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "commander": "^5.1.0", + "joi": "^17.9.2", + "react-helmet-async": "^1.3.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1", + "webpack-merge": "^5.9.0" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@docusaurus/module-type-aliases/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@docusaurus/preset-classic": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.1.0.tgz", @@ -3779,6 +3812,25 @@ "react-dom": "^18.0.0" } }, + "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/module-type-aliases": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.1.0.tgz", + "integrity": "sha512-XUl7Z4PWlKg4l6KF05JQ3iDHQxnPxbQUqTNKvviHyuHdlalOFv6qeDAm7IbzyQPJD5VA6y4dpRbTWSqP9ClwPg==", + "dependencies": { + "@docusaurus/react-loadable": "5.5.2", + "@docusaurus/types": "3.1.0", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "@types/react-router-dom": "*", + "react-helmet-async": "*", + "react-loadable": "npm:@docusaurus/react-loadable@5.5.2" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/plugin-content-blog": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.1.0.tgz", @@ -8606,48 +8658,48 @@ } }, "node_modules/@prisma/debug": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.1.tgz", - "integrity": "sha512-yrVSO/YZOxdeIxcBtZ5BaNqUfPrZkNsAKQIQg36cJKMxj/VYK3Vk5jMKkI+gQLl0KReo1YvX8GWKfV788SELjw==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.8.1.tgz", + "integrity": "sha512-tjuw7eA0Us3T42jx9AmAgL58rzwzpFGYc3R7Y4Ip75EBYrKMBA1YihuWMcBC92ILmjlQ/u3p8VxcIE0hr+fZfg==", "devOptional": true }, "node_modules/@prisma/engines": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.1.tgz", - "integrity": "sha512-R+Pqbra8tpLP2cvyiUpx+SIKglav3nTCpA+rn6826CThviQ8yvbNG0s8jNpo51vS9FuZO3pOkARqG062vKX7uA==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.8.1.tgz", + "integrity": "sha512-TJgYLRrZr56uhqcXO4GmP5be+zjCIHtLDK20Cnfg+o9d905hsN065QOL+3Z0zQAy6YD31Ol4u2kzSfRmbJv/uA==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/debug": "5.7.1", - "@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5", - "@prisma/fetch-engine": "5.7.1", - "@prisma/get-platform": "5.7.1" + "@prisma/debug": "5.8.1", + "@prisma/engines-version": "5.8.1-1.78caf6feeaed953168c64e15a249c3e9a033ebe2", + "@prisma/fetch-engine": "5.8.1", + "@prisma/get-platform": "5.8.1" } }, "node_modules/@prisma/engines-version": { - "version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5.tgz", - "integrity": "sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==", + "version": "5.8.1-1.78caf6feeaed953168c64e15a249c3e9a033ebe2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.8.1-1.78caf6feeaed953168c64e15a249c3e9a033ebe2.tgz", + "integrity": "sha512-f5C3JM3l9yhGr3cr4FMqWloFaSCpNpMi58Om22rjD2DOz3owci2mFdFXMgnAGazFPKrCbbEhcxdsRfspEYRoFQ==", "devOptional": true }, "node_modules/@prisma/fetch-engine": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.1.tgz", - "integrity": "sha512-9ELauIEBkIaEUpMIYPRlh5QELfoC6pyHolHVQgbNxglaINikZ9w9X7r1TIePAcm05pCNp2XPY1ObQIJW5nYfBQ==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.8.1.tgz", + "integrity": "sha512-+bgjjoSFa6uYEbAPlklfoVSStOEfcpheOjoBoNsNNSQdSzcwE2nM4Q0prun0+P8/0sCHo18JZ9xqa8gObvgOUw==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.7.1", - "@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5", - "@prisma/get-platform": "5.7.1" + "@prisma/debug": "5.8.1", + "@prisma/engines-version": "5.8.1-1.78caf6feeaed953168c64e15a249c3e9a033ebe2", + "@prisma/get-platform": "5.8.1" } }, "node_modules/@prisma/get-platform": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.1.tgz", - "integrity": "sha512-eDlswr3a1m5z9D/55Iyt/nZqS5UpD+DZ9MooBB3hvrcPhDQrcf9m4Tl7buy4mvAtrubQ626ECtb8c6L/f7rGSQ==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.8.1.tgz", + "integrity": "sha512-wnA+6HTFcY+tkykMokix9GiAkaauPC5W/gg0O5JB0J8tCTNWrqpnQ7AsaGRfkYUbeOIioh6woDjQrGTTRf1Zag==", "devOptional": true, "dependencies": { - "@prisma/debug": "5.7.1" + "@prisma/debug": "5.8.1" } }, "node_modules/@scure/base": { @@ -9212,9 +9264,9 @@ } }, "node_modules/@swc/core": { - "version": "1.3.99", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.99.tgz", - "integrity": "sha512-8O996RfuPC4ieb4zbYMfbyCU9k4gSOpyCNnr7qBQ+o7IEmh8JCV6B8wwu+fT/Om/6Lp34KJe1IpJ/24axKS6TQ==", + "version": "1.3.105", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.105.tgz", + "integrity": "sha512-me2VZyr3OjqRpFrYQJJYy7x/zbFSl9nt+MAGnIcBtjDsN00iTVqEaKxBjPBFQV9BDAgPz2SRWes/DhhVm5SmMw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -9229,15 +9281,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.99", - "@swc/core-darwin-x64": "1.3.99", - "@swc/core-linux-arm64-gnu": "1.3.99", - "@swc/core-linux-arm64-musl": "1.3.99", - "@swc/core-linux-x64-gnu": "1.3.99", - "@swc/core-linux-x64-musl": "1.3.99", - "@swc/core-win32-arm64-msvc": "1.3.99", - "@swc/core-win32-ia32-msvc": "1.3.99", - "@swc/core-win32-x64-msvc": "1.3.99" + "@swc/core-darwin-arm64": "1.3.105", + "@swc/core-darwin-x64": "1.3.105", + "@swc/core-linux-arm-gnueabihf": "1.3.105", + "@swc/core-linux-arm64-gnu": "1.3.105", + "@swc/core-linux-arm64-musl": "1.3.105", + "@swc/core-linux-x64-gnu": "1.3.105", + "@swc/core-linux-x64-musl": "1.3.105", + "@swc/core-win32-arm64-msvc": "1.3.105", + "@swc/core-win32-ia32-msvc": "1.3.105", + "@swc/core-win32-x64-msvc": "1.3.105" }, "peerDependencies": { "@swc/helpers": "^0.5.0" @@ -9249,9 +9302,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.99", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.99.tgz", - "integrity": "sha512-Qj7Jct68q3ZKeuJrjPx7k8SxzWN6PqLh+VFxzA+KwLDpQDPzOlKRZwkIMzuFjLhITO4RHgSnXoDk/Syz0ZeN+Q==", + "version": "1.3.105", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.105.tgz", + "integrity": "sha512-buWeweLVDXXmcnfIemH4PGnpjwsDTUGitnPchdftb0u1FU8zSSP/lw/pUCBDG/XvWAp7c/aFxgN4CyG0j7eayA==", "cpu": [ "arm64" ], @@ -9265,9 +9318,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.3.99", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.99.tgz", - "integrity": "sha512-wR7m9QVJjgiBu1PSOHy7s66uJPa45Kf9bZExXUL+JAa9OQxt5y+XVzr+n+F045VXQOwdGWplgPnWjgbUUHEVyw==", + "version": "1.3.105", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.105.tgz", + "integrity": "sha512-hFmXPApqjA/8sy/9NpljHVaKi1OvL9QkJ2MbbTCCbJERuHMpMUeMBUWipHRfepGHFhU+9B9zkEup/qJaJR4XIg==", "cpu": [ "x64" ], @@ -9280,10 +9333,26 @@ "node": ">=10" } }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.3.105", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.105.tgz", + "integrity": "sha512-mwXyMC41oMKkKrPpL8uJpOxw7fyfQoVtIw3Y5p0Blabk+espNYqix0E8VymHdRKuLmM//z5wVmMsuHdGBHvZeg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.99", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.99.tgz", - "integrity": "sha512-gcGv1l5t0DScEONmw5OhdVmEI/o49HCe9Ik38zzH0NtDkc+PDYaCcXU5rvfZP2qJFaAAr8cua8iJcOunOSLmnA==", + "version": "1.3.105", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.105.tgz", + "integrity": "sha512-H7yEIVydnUtqBSUxwmO6vpIQn7j+Rr0DF6ZOORPyd/SFzQJK9cJRtmJQ3ZMzlJ1Bb+1gr3MvjgLEnmyCYEm2Hg==", "cpu": [ "arm64" ], @@ -9297,9 +9366,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.99", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.99.tgz", - "integrity": "sha512-XL1/eUsTO8BiKsWq9i3iWh7H99iPO61+9HYiWVKhSavknfj4Plbn+XyajDpxsauln5o8t+BRGitymtnAWJM4UQ==", + "version": "1.3.105", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.105.tgz", + "integrity": "sha512-Jg7RTFT3pGFdGt5elPV6oDkinRy7q9cXpenjXnJnM2uvx3jOwnsAhexPyCDHom8SHL0j+9kaLLC66T3Gz1E4UA==", "cpu": [ "arm64" ], @@ -9313,9 +9382,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.99", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.99.tgz", - "integrity": "sha512-fGrXYE6DbTfGNIGQmBefYxSk3rp/1lgbD0nVg4rl4mfFRQPi7CgGhrrqSuqZ/ezXInUIgoCyvYGWFSwjLXt/Qg==", + "version": "1.3.105", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.105.tgz", + "integrity": "sha512-DJghplpyusAmp1X5pW/y93MmS/u83Sx5GrpJxI6KLPa82+NItTgMcl8KBQmW5GYAJpVKZyaIvBanS5TdR8aN2w==", "cpu": [ "x64" ], @@ -9329,9 +9398,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.99", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.99.tgz", - "integrity": "sha512-kvgZp/mqf3IJ806gUOL6gN6VU15+DfzM1Zv4Udn8GqgXiUAvbQehrtruid4Snn5pZTLj4PEpSCBbxgxK1jbssA==", + "version": "1.3.105", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.105.tgz", + "integrity": "sha512-wD5jL2dZH/5nPNssBo6jhOvkI0lmWnVR4vnOXWjuXgjq1S0AJpO5jdre/6pYLmf26hft3M42bteDnjR4AAZ38w==", "cpu": [ "x64" ], @@ -9345,9 +9414,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.99", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.99.tgz", - "integrity": "sha512-yt8RtZ4W/QgFF+JUemOUQAkVW58cCST7mbfKFZ1v16w3pl3NcWd9OrtppFIXpbjU1rrUX2zp2R7HZZzZ2Zk/aQ==", + "version": "1.3.105", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.105.tgz", + "integrity": "sha512-UqJtwILUHRw2+3UTPnRkZrzM/bGdQtbR4UFdp79mZQYfryeOUVNg7aJj/bWUTkKtLiZ3o+FBNrM/x2X1mJX5bA==", "cpu": [ "arm64" ], @@ -9361,9 +9430,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.99", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.99.tgz", - "integrity": "sha512-62p5fWnOJR/rlbmbUIpQEVRconICy5KDScWVuJg1v3GPLBrmacjphyHiJC1mp6dYvvoEWCk/77c/jcQwlXrDXw==", + "version": "1.3.105", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.105.tgz", + "integrity": "sha512-Z95C6vZgBEJ1snidYyjVKnVWiy/ZpPiIFIXGWkDr4ZyBgL3eZX12M6LzZ+NApHKffrbO4enbFyFomueBQgS2oA==", "cpu": [ "ia32" ], @@ -9377,9 +9446,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.99", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.99.tgz", - "integrity": "sha512-PdppWhkoS45VGdMBxvClVgF1hVjqamtvYd82Gab1i4IV45OSym2KinoDCKE1b6j3LwBLOn2J9fvChGSgGfDCHQ==", + "version": "1.3.105", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.105.tgz", + "integrity": "sha512-3J8fkyDPFsS3mszuYUY4Wfk7/B2oio9qXUwF3DzOs2MK+XgdyMLIptIxL7gdfitXJBH8k39uVjrIw1JGJDjyFA==", "cpu": [ "x64" ], @@ -10183,15 +10252,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz", - "integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz", + "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/scope-manager": "6.19.1", + "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/typescript-estree": "6.19.1", + "@typescript-eslint/visitor-keys": "6.19.1", "debug": "^4.3.4" }, "engines": { @@ -10211,13 +10280,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz", - "integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", + "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0" + "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/visitor-keys": "6.19.1" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -10370,9 +10439,9 @@ "dev": true }, "node_modules/@typescript-eslint/types": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz", - "integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", + "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -10383,16 +10452,17 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz", - "integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", + "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/visitor-keys": "6.19.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, @@ -10409,6 +10479,15 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -10421,6 +10500,21 @@ "node": ">=10" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -10600,12 +10694,12 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz", - "integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==", + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", + "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/types": "6.19.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -12304,6 +12398,72 @@ "@colors/colors": "1.5.0" } }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.0.0.tgz", + "integrity": "sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -13848,6 +14008,15 @@ "node": ">=12" } }, + "node_modules/date-fns": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -16306,6 +16475,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -17238,6 +17419,21 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -19351,63 +19547,365 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "node_modules/lint-staged": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.0.tgz", + "integrity": "sha512-TFZzUEV00f+2YLaVPWBWGAMq7So6yQx+GG8YRMDeOEIf95Zn5RyiLMsEiX4KTNl9vq/w+NqRJkLA1kPIo15ufQ==", + "dev": true, + "dependencies": { + "chalk": "5.3.0", + "commander": "11.1.0", + "debug": "4.3.4", + "execa": "8.0.1", + "lilconfig": "3.0.0", + "listr2": "8.0.0", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, "engines": { - "node": ">=6.11.5" + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" } }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, "engines": { - "node": ">=8.9.0" + "node": ">=16" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/lint-staged/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "node_modules/lint-staged/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + "node_modules/lint-staged/node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/listr2": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.0.tgz", + "integrity": "sha512-u8cusxAcyqAiQ2RhYvV7kRKNLgUvtObIbhOX2NCXqvp1UU32xIg5CT22ykS2TPKJXZWJwtK3IKLiqAGlGNE+Zg==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.0.0", + "rfdc": "^1.3.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.0.0.tgz", + "integrity": "sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "dev": true }, "node_modules/lodash.memoize": { @@ -19442,6 +19940,193 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", + "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^6.2.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^7.0.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "dev": true, + "dependencies": { + "type-fest": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.0.0.tgz", + "integrity": "sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -22720,6 +23405,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -23696,13 +24393,13 @@ } }, "node_modules/prisma": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.1.tgz", - "integrity": "sha512-ekho7ziH0WEJvC4AxuJz+ewRTMskrebPcrKuBwcNzVDniYxx+dXOGcorNeIb9VEMO5vrKzwNYvhD271Ui2jnNw==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.8.1.tgz", + "integrity": "sha512-N6CpjzECnUHZ5beeYpDzkt2rYpEdAeqXX2dweu6BoQaeYkNZrC/WJHM+5MO/uidFHTak8QhkPKBWck1o/4MD4A==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.7.1" + "@prisma/engines": "5.8.1" }, "bin": { "prisma": "build/index.js" @@ -24952,6 +25649,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -25639,6 +26342,46 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -25850,6 +26593,15 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", diff --git a/package.json b/package.json index 50f54b235..568fd2e31 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "devDependencies": { "@automock/adapters.nestjs": "^2.1.0", "@automock/jest": "^2.1.0", - "@docusaurus/module-type-aliases": "3.1.0", + "@docusaurus/module-type-aliases": "3.0.0", "@docusaurus/tsconfig": "3.0.0", "@docusaurus/types": "3.1.0", "@golevelup/ts-jest": "^0.4.0", @@ -27,7 +27,7 @@ "@nx/workspace": "17.2.8", "@playwright/test": "^1.36.0", "@swc-node/register": "~1.6.7", - "@swc/core": "~1.3.85", + "@swc/core": "~1.3.105", "@testing-library/react": "14.1.2", "@types/jest": "^29.5.11", "@types/lodash": "^4.14.202", @@ -38,7 +38,7 @@ "@types/supertest": "^6.0.2", "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.18.0", - "@typescript-eslint/parser": "^6.9.1", + "@typescript-eslint/parser": "^6.19.1", "babel-jest": "^29.4.1", "dotenv-cli": "^7.3.0", "eslint": "~8.46.0", @@ -49,14 +49,16 @@ "eslint-plugin-playwright": "^0.21.0", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", + "husky": "8.0.3", "jest": "^29.4.1", "jest-environment-jsdom": "^29.4.1", "jest-environment-node": "^29.4.1", + "lint-staged": "15.2.0", "nock": "^13.4.0", "nx": "17.1.3", "prettier": "^2.6.2", "prettier-plugin-organize-imports": "^3.2.4", - "prisma": "^5.7.1", + "prisma": "^5.8.1", "supertest": "^6.3.4", "ts-jest": "^29.1.0", "ts-node": "10.9.1", @@ -84,6 +86,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "clsx": "^1.2.1", + "date-fns": "^3.3.1", "lodash": "^4.17.21", "prism-react-renderer": "^2.3.1", "react": "18.2.0", diff --git a/packages/authz-shared/src/lib/util/__test__/unit/hash-request.util.spec.ts b/packages/authz-shared/src/lib/util/__test__/unit/hash-request.util.spec.ts new file mode 100644 index 000000000..2aeefa1a9 --- /dev/null +++ b/packages/authz-shared/src/lib/util/__test__/unit/hash-request.util.spec.ts @@ -0,0 +1,40 @@ +import { hashRequest } from '../../hash-request.util' + +describe('hashRequest', () => { + it('hashes the given object', () => { + expect( + hashRequest({ + a: 'a', + b: 1, + c: false + }) + ).toEqual('7372a4267af39345919d5d26984da5e387d8d93b25283c9740b3bd43841bcf49') + }) + + it('hashes the given array', () => { + expect(hashRequest(['a', 1, false])).toEqual('cdd23dea0598c5ffc66b6a53f9dc7448a87b47454f209caa310e21da91754173') + }) + + it('hashes two objects deterministically', () => { + const a = { + a: 'a', + b: 1, + c: false, + d: { + a: 'a', + b: 1 + } + } + const b = { + c: false, + b: 1, + d: { + b: 1, + a: 'a' + }, + a: 'a' + } + + expect(hashRequest(a)).toEqual(hashRequest(b)) + }) +}) diff --git a/packages/authz-shared/src/lib/util/hash-request.util.ts b/packages/authz-shared/src/lib/util/hash-request.util.ts index 42fa62e27..b3f30ccf7 100644 --- a/packages/authz-shared/src/lib/util/hash-request.util.ts +++ b/packages/authz-shared/src/lib/util/hash-request.util.ts @@ -1,6 +1,25 @@ import { createHash } from 'crypto' import { stringify } from './json.util' +const sort = (value: unknown): unknown => { + if (typeof value !== 'object' || value === null || value === undefined) { + return value + } + + if (Array.isArray(value)) { + return value.map(sort) + } + + return Object.keys(value) + .sort() + .reduce((acc, key) => { + return { + ...acc, + [key]: sort((value as Record)[key]) + } + }, {}) +} + /** * Returns the hexadecimal of the given value SHA256 hash. * @@ -10,5 +29,7 @@ import { stringify } from './json.util' * @returns object's hash */ export const hashRequest = (value: unknown): string => { - return createHash('sha256').update(stringify(value)).digest('hex') + return createHash('sha256') + .update(stringify(sort(value))) + .digest('hex') } diff --git a/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts b/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts index 15be0c7ed..662d353fb 100644 --- a/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts +++ b/packages/transaction-request-intent/src/lib/__test__/unit/decoders.spec.ts @@ -1,5 +1,6 @@ import Decoder from '../../decoders/Decoder' -import { InputType, Intents } from '../../domain' +import { InputType, Intents, TransactionStatus } from '../../domain' +import { buildTransactionKey, buildTransactionRegistry } from '../../utils' import { mockErc1155BatchSafeTransferFrom, mockErc1155SafeTransferFrom, @@ -9,8 +10,10 @@ import { describe('decode', () => { let decoder: Decoder + + const transactionRegistry = buildTransactionRegistry([]) beforeEach(() => { - decoder = new Decoder() + decoder = new Decoder({}) }) describe('transaction request input', () => { describe('transfers', () => { @@ -49,6 +52,25 @@ describe('decode', () => { token: 'eip155:137/slip44/966' }) }) + it('decodes approve token allowance', () => { + const decoded = decoder.decode({ + type: InputType.TRANSACTION_REQUEST, + txRequest: { + to: '0x031d8C0cA142921c459bCB28104c0FF37928F9eD', + data: '0x095ea7b30000000000000000000000001111111254eeb25477b68fb85ed929f73a9605821984862f285d9925ca94e9e52a28867736f1114e8b27b3300dbbaf71ed200b67', + from: '0xEd123cf8e3bA51c6C15DA1eAc74B2b5DEEA31448', + chainId: 137, + nonce: 10 + } + }) + expect(decoded).toEqual({ + type: Intents.APPROVE_TOKEN_ALLOWANCE, + from: 'eip155:137:0xed123cf8e3ba51c6c15da1eac74b2b5deea31448', + token: 'eip155:137:0x031d8c0ca142921c459bcb28104c0ff37928f9ed', + amount: '11541971132511365478906515907109950360107522067033065608472376982619868367719', + spender: 'eip155:137:0x1111111254eeb25477b68fb85ed929f73a960582' + }) + }) it('defaults to contract call intent', () => { const decoded = decoder.decode({ type: InputType.TRANSACTION_REQUEST, @@ -68,24 +90,34 @@ describe('decode', () => { }) }) }) - // describe('transaction management', () => { - // it('decodes retry transaction', () => { - // }); - // it('decodes cancel transaction', () => { - // }); - // }); - // describe('contract creation', () => { - // it('decodes safe wallet creation deployment', () => { - // }); - // it('decodes erc4337 wallet deployment', () => { - // }); - // it('defaults to contract deployment intent', () => { - // }); - // }) - // it('decodes approve token allowance', () => { - // }); - // it('defaults to contract call intent', () => { - // }); + describe('transaction management', () => { + // SET A FAILED TO TRANSACTION ON FIRST MOCK DATA + const key = buildTransactionKey(mockErc20Transfer.input.txRequest) + transactionRegistry.set(key, TransactionStatus.FAILED) + it('should find the transaction in the registry', () => { + const trxStatus = transactionRegistry.get(buildTransactionKey(mockErc20Transfer.input.txRequest)) + expect(trxStatus).toEqual(TransactionStatus.FAILED) + }) + // NOW ITS A PENDING + it('decodes retry transaction', () => {}) + it('decodes cancel transaction', () => { + transactionRegistry.set(key, TransactionStatus.PENDING) + const decoded = decoder.decode({ + type: InputType.TRANSACTION_REQUEST, + txRequest: mockErc20Transfer.input.txRequest, + transactionRegistry + }) + expect(decoded).toEqual({ + type: Intents.CANCEL_TRANSACTION, + originalIntent: mockErc20Transfer.intent + }) + }) + }) + describe('contract creation', () => { + it('decodes safe wallet creation deployment', () => {}) + it('decodes erc4337 wallet deployment', () => {}) + it('defaults to contract deployment intent', () => {}) + }) }) // describe('message and typed data input', () => { // it('decodes message', () => { diff --git a/packages/transaction-request-intent/src/lib/decoders/Decoder.ts b/packages/transaction-request-intent/src/lib/decoders/Decoder.ts index ba124e2e4..4a461342a 100644 --- a/packages/transaction-request-intent/src/lib/decoders/Decoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/Decoder.ts @@ -1,26 +1,43 @@ import { ContractCallInput, + ContractRegistry, DecodeInput, InputType, Intents, SafeDecodeOutput, TransactionCategory, - TransactionInput + TransactionInput, + TransactionRegistry, + TransactionStatus } from '../domain' import { TransactionRequestIntentError } from '../error' import { Intent } from '../intent.types' import { isSupportedMethodId } from '../typeguards' -import { getCategory, getMethodId, getTransactionIntentType } from '../utils' +import { getCategory, getMethodId, getTransactionIntentType, transactionLookup } from '../utils' import { validateContractInteractionInput, validateNativeTransferInput } from '../validators' -import ApproveTokenAllowanceDecoder from './ApproveAllowanceDecoder' -import CallContractDecoder from './CallContractDecoder' import DecoderStrategy from './DecoderStrategy' -import ERC1155TransferDecoder from './Erc1155TransferDecoder' -import Erc20TransferDecoder from './Erc20TransferDecoder' -import Erc721TransferDecoder from './Erc721TransferDecoder' -import NativeTransferDecoder from './NativeTransferDecoder' +import NativeTransferDecoder from './native/NativeTransferDecoder' +import ApproveTokenAllowanceDecoder from './transaction/interaction/ApproveAllowanceDecoder' +import CallContractDecoder from './transaction/interaction/CallContractDecoder' +import ERC1155TransferDecoder from './transaction/interaction/Erc1155TransferDecoder' +import Erc20TransferDecoder from './transaction/interaction/Erc20TransferDecoder' +import Erc721TransferDecoder from './transaction/interaction/Erc721TransferDecoder' + +export type DecoderOption = { + contractRegistry?: ContractRegistry + transactionRegistry?: TransactionRegistry +} export default class Decoder { + contractRegistry?: ContractRegistry + + transactionRegistry?: TransactionRegistry + + constructor(option?: DecoderOption) { + this.contractRegistry = option?.contractRegistry + this.transactionRegistry = option?.transactionRegistry + } + #findContractCallStrategy(input: ContractCallInput, intent: Intents): DecoderStrategy { if (!isSupportedMethodId(input.methodId)) { return new CallContractDecoder(input) @@ -41,7 +58,7 @@ export default class Decoder { } #findTransactionStrategy(input: TransactionInput): DecoderStrategy { - const { txRequest, contractRegistry, transactionRegistry } = input + const { txRequest, contractRegistry } = input const { data, to } = txRequest const methodId = getMethodId(data) const category = getCategory(methodId, to) @@ -56,8 +73,7 @@ export default class Decoder { const intent = getTransactionIntentType({ methodId, txRequest: validatedTxRequest, - contractRegistry, - transactionRegistry + contractRegistry }) return this.#findContractCallStrategy(validatedTxRequest, intent) } @@ -68,18 +84,37 @@ export default class Decoder { } } - #findStrategy(input: DecodeInput): DecoderStrategy { - switch (input.type) { - case InputType.TRANSACTION_REQUEST: - return this.#findTransactionStrategy(input) + #wrapTransactionManagementIntents(intent: Intent, input: TransactionInput): Intent { + const { txRequest, transactionRegistry } = input + const trxStatus = transactionLookup(txRequest, transactionRegistry) + switch (trxStatus) { + case TransactionStatus.FAILED: + return { + type: Intents.RETRY_TRANSACTION, + originalIntent: intent + } + case TransactionStatus.PENDING: + return { + type: Intents.CANCEL_TRANSACTION, + originalIntent: intent + } + case TransactionStatus.SUCCESS: + throw new Error('Transaction already mined') default: - throw new Error('Invalid input type') + return intent } } public decode(input: DecodeInput): Intent { - const strategy = this.#findStrategy(input) - return strategy.decode() + switch (input.type) { + case InputType.TRANSACTION_REQUEST: { + const strategy = this.#findTransactionStrategy(input) + const decoded = strategy.decode() + return this.#wrapTransactionManagementIntents(decoded, input) + } + default: + throw new Error('Invalid input type') + } } public safeDecode(input: DecodeInput): SafeDecodeOutput { diff --git a/packages/transaction-request-intent/src/lib/decoders/NativeTransferDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/native/NativeTransferDecoder.ts similarity index 77% rename from packages/transaction-request-intent/src/lib/decoders/NativeTransferDecoder.ts rename to packages/transaction-request-intent/src/lib/decoders/native/NativeTransferDecoder.ts index 5ad1e2732..db667935d 100644 --- a/packages/transaction-request-intent/src/lib/decoders/NativeTransferDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/native/NativeTransferDecoder.ts @@ -1,8 +1,8 @@ -import { Caip19, encodeEoaAccountId } from '../caip' -import { Intents, NativeTransferInput } from '../domain' -import { TransactionRequestIntentError } from '../error' -import { TransferNative } from '../intent.types' -import DecoderStrategy from './DecoderStrategy' +import { Caip19, encodeEoaAccountId } from '../../caip' +import { Intents, NativeTransferInput } from '../../domain' +import { TransactionRequestIntentError } from '../../error' +import { TransferNative } from '../../intent.types' +import DecoderStrategy from '../DecoderStrategy' export default class NativeTransferDecoder extends DecoderStrategy { #input: NativeTransferInput diff --git a/packages/transaction-request-intent/src/lib/decoders/ApproveAllowanceDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/ApproveAllowanceDecoder.ts similarity index 73% rename from packages/transaction-request-intent/src/lib/decoders/ApproveAllowanceDecoder.ts rename to packages/transaction-request-intent/src/lib/decoders/transaction/interaction/ApproveAllowanceDecoder.ts index d5f90a85e..5a2624519 100644 --- a/packages/transaction-request-intent/src/lib/decoders/ApproveAllowanceDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/ApproveAllowanceDecoder.ts @@ -1,9 +1,9 @@ -import { encodeEoaAccountId } from '../caip' -import { ContractCallInput, Intents } from '../domain' -import { ApproveAllowanceParams } from '../extraction/types' -import { ApproveTokenAllowance } from '../intent.types' -import { isSupportedMethodId } from '../typeguards' -import DecoderStrategy from './DecoderStrategy' +import { encodeEoaAccountId } from '../../../caip' +import { ContractCallInput, Intents } from '../../../domain' +import { ApproveAllowanceParams } from '../../../extraction/types' +import { ApproveTokenAllowance } from '../../../intent.types' +import { isSupportedMethodId } from '../../../typeguards' +import DecoderStrategy from '../../DecoderStrategy' export default class ApproveTokenAllowanceDecoder extends DecoderStrategy { #input: ContractCallInput diff --git a/packages/transaction-request-intent/src/lib/decoders/CallContractDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/CallContractDecoder.ts similarity index 71% rename from packages/transaction-request-intent/src/lib/decoders/CallContractDecoder.ts rename to packages/transaction-request-intent/src/lib/decoders/transaction/interaction/CallContractDecoder.ts index e8e4b696a..20a6677f7 100644 --- a/packages/transaction-request-intent/src/lib/decoders/CallContractDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/CallContractDecoder.ts @@ -1,7 +1,7 @@ -import { encodeEoaAccountId } from '../caip' -import { ContractCallInput, Intents } from '../domain' -import { CallContract } from '../intent.types' -import DecoderStrategy from './DecoderStrategy' +import { encodeEoaAccountId } from '../../../caip' +import { ContractCallInput, Intents } from '../../../domain' +import { CallContract } from '../../../intent.types' +import DecoderStrategy from '../../DecoderStrategy' export default class CallContractDecoder extends DecoderStrategy { #input: ContractCallInput diff --git a/packages/transaction-request-intent/src/lib/decoders/Erc1155TransferDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc1155TransferDecoder.ts similarity index 86% rename from packages/transaction-request-intent/src/lib/decoders/Erc1155TransferDecoder.ts rename to packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc1155TransferDecoder.ts index abe6a22a5..fbcf4fde2 100644 --- a/packages/transaction-request-intent/src/lib/decoders/Erc1155TransferDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc1155TransferDecoder.ts @@ -1,10 +1,10 @@ -import { encodeEoaAccountId, encodeEoaAssetId } from '../caip' -import { AssetTypeEnum, ContractCallInput, EipStandardEnum, Intents } from '../domain' -import { Erc1155SafeTransferFromParams, SafeBatchTransferFromParams } from '../extraction/types' -import { ERC1155Transfer, TransferErc1155 } from '../intent.types' -import { SupportedMethodId } from '../supported-methods' -import { isSupportedMethodId } from '../typeguards' -import DecoderStrategy from './DecoderStrategy' +import { encodeEoaAccountId, encodeEoaAssetId } from '../../../caip' +import { AssetTypeEnum, ContractCallInput, EipStandardEnum, Intents } from '../../../domain' +import { Erc1155SafeTransferFromParams, SafeBatchTransferFromParams } from '../../../extraction/types' +import { ERC1155Transfer, TransferErc1155 } from '../../../intent.types' +import { SupportedMethodId } from '../../../supported-methods' +import { isSupportedMethodId } from '../../../typeguards' +import DecoderStrategy from '../../DecoderStrategy' export default class ERC1155TransferDecoder extends DecoderStrategy { #input: ContractCallInput diff --git a/packages/transaction-request-intent/src/lib/decoders/Erc20TransferDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc20TransferDecoder.ts similarity index 73% rename from packages/transaction-request-intent/src/lib/decoders/Erc20TransferDecoder.ts rename to packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc20TransferDecoder.ts index ab4a827ba..f82943c78 100644 --- a/packages/transaction-request-intent/src/lib/decoders/Erc20TransferDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc20TransferDecoder.ts @@ -1,9 +1,9 @@ -import { encodeEoaAccountId } from '../caip' -import { ContractCallInput, Intents } from '../domain' -import { TransferParams } from '../extraction/types' -import { TransferErc20 } from '../intent.types' -import { isSupportedMethodId } from '../typeguards' -import DecoderStrategy from './DecoderStrategy' +import { encodeEoaAccountId } from '../../../caip' +import { ContractCallInput, Intents } from '../../../domain' +import { TransferParams } from '../../../extraction/types' +import { TransferErc20 } from '../../../intent.types' +import { isSupportedMethodId } from '../../../typeguards' +import DecoderStrategy from '../../DecoderStrategy' export default class Erc20TransferDecoder extends DecoderStrategy { #input: ContractCallInput diff --git a/packages/transaction-request-intent/src/lib/decoders/Erc721TransferDecoder.ts b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc721TransferDecoder.ts similarity index 76% rename from packages/transaction-request-intent/src/lib/decoders/Erc721TransferDecoder.ts rename to packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc721TransferDecoder.ts index 654925f61..aade42661 100644 --- a/packages/transaction-request-intent/src/lib/decoders/Erc721TransferDecoder.ts +++ b/packages/transaction-request-intent/src/lib/decoders/transaction/interaction/Erc721TransferDecoder.ts @@ -1,11 +1,11 @@ import { AbiParameter } from 'viem' -import { encodeEoaAccountId, encodeEoaAssetId } from '../caip' -import { AssetTypeEnum, ContractCallInput, EipStandardEnum, Intents } from '../domain' -import { Erc721SafeTransferFromParams } from '../extraction/types' -import { TransferErc721 } from '../intent.types' -import { Erc721SafeTransferFromAbiParameters } from '../supported-methods' -import { isSupportedMethodId } from '../typeguards' -import DecoderStrategy from './DecoderStrategy' +import { encodeEoaAccountId, encodeEoaAssetId } from '../../../caip' +import { AssetTypeEnum, ContractCallInput, EipStandardEnum, Intents } from '../../../domain' +import { Erc721SafeTransferFromParams } from '../../../extraction/types' +import { TransferErc721 } from '../../../intent.types' +import { Erc721SafeTransferFromAbiParameters } from '../../../supported-methods' +import { isSupportedMethodId } from '../../../typeguards' +import DecoderStrategy from '../../DecoderStrategy' export default class Erc721TransferDecoder extends DecoderStrategy { abi: AbiParameter[] = Erc721SafeTransferFromAbiParameters diff --git a/packages/transaction-request-intent/src/lib/domain.ts b/packages/transaction-request-intent/src/lib/domain.ts index b79219ebc..08cb7bcbe 100644 --- a/packages/transaction-request-intent/src/lib/domain.ts +++ b/packages/transaction-request-intent/src/lib/domain.ts @@ -46,13 +46,14 @@ export type TypedDataInput = { typedData: TypedData } -export type ContractRegistry = { - [key: Caip10]: AssetTypeEnum -} +export type ContractRegistryInput = { + contract: Caip10 | { address: Address; chainId: number } + assetType: AssetTypeEnum +}[] +export type ContractRegistry = Map -export type TransactionRegistry = { - [key: string]: TransactionStatus // tbd -} +export type TransactionKey = `${Caip10}-${number}` +export type TransactionRegistry = Map export type TransactionInput = { type: InputType.TRANSACTION_REQUEST @@ -123,6 +124,7 @@ export enum TransactionCategory { } export enum TransactionStatus { + SUCCESS = 'success', PENDING = 'pending', FAILED = 'failed' } diff --git a/packages/transaction-request-intent/src/lib/intent.types.ts b/packages/transaction-request-intent/src/lib/intent.types.ts index 0e47f3041..412544345 100644 --- a/packages/transaction-request-intent/src/lib/intent.types.ts +++ b/packages/transaction-request-intent/src/lib/intent.types.ts @@ -91,13 +91,11 @@ export type DeploySafeWallet = { export type RetryTransaction = { type: Intents.RETRY_TRANSACTION - from: Caip10 originalIntent: Intent } export type CancelTransaction = { type: Intents.CANCEL_TRANSACTION - from: Caip10 originalIntent: Intent } @@ -132,3 +130,5 @@ export type Intent = | TransferErc1155 | CallContract | ApproveTokenAllowance + | RetryTransaction + | CancelTransaction diff --git a/packages/transaction-request-intent/src/lib/supported-methods.ts b/packages/transaction-request-intent/src/lib/supported-methods.ts index a9c8b7242..6edc89a30 100644 --- a/packages/transaction-request-intent/src/lib/supported-methods.ts +++ b/packages/transaction-request-intent/src/lib/supported-methods.ts @@ -95,7 +95,7 @@ export const ApproveAllowanceAbiParameters: AbiParameter[] = [ export enum SupportedMethodId { TRANSFER = '0xa9059cbb', TRANSFER_FROM = '0x23b872dd', - APPROVE = '0xa613914d', + APPROVE = '0x095ea7b3', SAFE_TRANSFER_FROM = '0x42842e0e', SAFE_TRANSFER_FROM_WITH_BYTES = '0xb88d4fde', SAFE_BATCH_TRANSFER_FROM = '0x2eb2c2d6', @@ -124,7 +124,7 @@ export type MethodsMapping = { abi: AbiParameter[] transformer: (params: unknown[]) => StandardMethodsParams[K] assetType: AssetTypeEnum - intent?: Intents + intent: Intents } } @@ -140,7 +140,8 @@ export const SUPPORTED_METHODS: MethodsMapping = { name: 'transferFrom', abi: TransferFromAbiParameters, transformer: TransferFromParamsTransform, - assetType: AssetTypeEnum.UNKNOWN + assetType: AssetTypeEnum.UNKNOWN, + intent: Intents.CALL_CONTRACT }, [SupportedMethodId.SAFE_TRANSFER_FROM]: { name: 'safeTransferFrom', @@ -181,12 +182,14 @@ export const SUPPORTED_METHODS: MethodsMapping = { name: 'approve', abi: ApproveAllowanceAbiParameters, transformer: ApproveAllowanceParamsTransform, - assetType: AssetTypeEnum.UNKNOWN + assetType: AssetTypeEnum.ERC20, + intent: Intents.APPROVE_TOKEN_ALLOWANCE }, [SupportedMethodId.NULL_METHOD_ID]: { - name: 'nativeTransfer', + name: 'empty data field', abi: [], transformer: () => ({}), - assetType: AssetTypeEnum.UNKNOWN + assetType: AssetTypeEnum.UNKNOWN, + intent: Intents.TRANSFER_NATIVE } } diff --git a/packages/transaction-request-intent/src/lib/typeguards.ts b/packages/transaction-request-intent/src/lib/typeguards.ts index c5a420eaa..d1300aabb 100644 --- a/packages/transaction-request-intent/src/lib/typeguards.ts +++ b/packages/transaction-request-intent/src/lib/typeguards.ts @@ -1,4 +1,6 @@ import { Hex } from 'viem' +import { Caip10, Caip19 } from './caip' +import { AssetTypeEnum } from './domain' import { SupportedMethodId } from './supported-methods' export const isString = (value: unknown): value is string => { @@ -69,6 +71,15 @@ export const isSupportedMethodId = (value: Hex): value is SupportedMethodId => { type AssertType = 'string' | 'bigint' | 'number' | 'boolean' | 'hex' +export const isAssetType = (value: unknown): value is AssetTypeEnum => + Object.values(AssetTypeEnum).includes(value as AssetTypeEnum) + +// TODO: refine these typeguards to be accurate, using reghex and test them +export const isCaip10 = (value: unknown): value is Caip10 => + isString(value) && value.startsWith('eip155') && !value.includes('eoa') +export const isCaip19 = (value: unknown): value is Caip19 => + isString(value) && value.startsWith('eip155') && value.includes('/') + export const assertArray = (value: unknown, type: AssertType): T[] => { if (!Array.isArray(value)) { throw new Error('Value is not an array') diff --git a/packages/transaction-request-intent/src/lib/utils.ts b/packages/transaction-request-intent/src/lib/utils.ts index fa1472d46..17db46d02 100644 --- a/packages/transaction-request-intent/src/lib/utils.ts +++ b/packages/transaction-request-intent/src/lib/utils.ts @@ -1,17 +1,20 @@ -import { Hex } from 'viem' -import { encodeEoaAccountId } from './caip' +import { TransactionRequest } from '@narval/authz-shared' +import { Hex, isAddress } from 'viem' +import { Caip10, encodeEoaAccountId } from './caip' import { AssetTypeEnum, ContractRegistry, + ContractRegistryInput, Intents, NULL_METHOD_ID, TransactionCategory, + TransactionKey, TransactionRegistry, TransactionStatus, ValidatedInput } from './domain' import { SUPPORTED_METHODS, SupportedMethodId } from './supported-methods' -import { assertHexString, isSupportedMethodId } from './typeguards' +import { assertHexString, isAssetType, isCaip10, isSupportedMethodId } from './typeguards' export const getMethodId = (data?: string): Hex => (data ? assertHexString(data.slice(0, 10)) : NULL_METHOD_ID) @@ -25,31 +28,94 @@ export const getCategory = (methodId: Hex, to?: Hex | null): TransactionCategory return TransactionCategory.CONTRACT_INTERACTION } -const contractTypeLookup = ( +export const buildContractRegistryEntry = ({ + chainId, + contractAddress, + assetType +}: { + chainId: number + contractAddress: string + assetType: string +}): { [key: Caip10]: AssetTypeEnum } => { + const registry: { [key: Caip10]: AssetTypeEnum } = {} + if (!isAddress(contractAddress) || !isAssetType(assetType)) { + throw new Error('Invalid contract registry entry') + } + const key = buildContractKey(chainId, contractAddress) + registry[key] = assetType + return registry +} + +export const buildContractRegistry = (input: ContractRegistryInput): ContractRegistry => { + const registry = new Map() + input.forEach(({ contract, assetType }) => { + if (isCaip10(contract)) { + registry.set(contract, assetType) + } else { + const key = buildContractKey(contract.chainId, contract.address) + registry.set(key, assetType) + } + }) + return registry +} + +export const buildContractKey = (chainId: number, contractAddress: Hex): Caip10 => + encodeEoaAccountId({ chainId, evmAccountAddress: contractAddress }) + +export const checkContractRegistry = (registry: Record) => { + Object.keys(registry).forEach((key) => { + if (!isCaip10(key)) { + throw new Error(`Invalid contract registry key: ${key}: ${registry[key]}`) + } + if (!isAssetType(registry[key])) { + throw new Error(`Invalid contract registry value: ${key}: ${registry[key]}`) + } + }) + return true +} + +export const contractTypeLookup = ( txRequest: ValidatedInput, - contractRegistry: ContractRegistry + contractRegistry?: ContractRegistry ): AssetTypeEnum | undefined => { - if ('to' in txRequest && txRequest.to) { - const key = encodeEoaAccountId({ - chainId: txRequest.chainId, - evmAccountAddress: txRequest.to - }) - return contractRegistry[key] + if ('to' in txRequest && txRequest.to && contractRegistry) { + const key = buildContractKey(txRequest.chainId, txRequest.to) + return contractRegistry.get(key) } return undefined } -export const transactionLookup = ( - txRequest: ValidatedInput, - transactionRegistry?: TransactionRegistry -): TransactionStatus | undefined => { +export const buildTransactionKey = (txRequest: TransactionRequest): TransactionKey => { + if (!txRequest.nonce) throw new Error('Invalid transaction request') const account = encodeEoaAccountId({ chainId: txRequest.chainId, evmAccountAddress: txRequest.from }) - const key = `${account}-${txRequest.nonce}` + return `${account}-${txRequest.nonce}` +} + +export type TransactionRegistryInput = { + txRequest: TransactionRequest + status: TransactionStatus +}[] + +export const buildTransactionRegistry = (input: TransactionRegistryInput): TransactionRegistry => { + const registry = new Map() + input.forEach(({ txRequest, status }) => { + const key = buildTransactionKey(txRequest) + registry.set(key, status) + }) + return registry +} + +export const transactionLookup = ( + txRequest: TransactionRequest, + transactionRegistry?: TransactionRegistry +): TransactionStatus | undefined => { + const key: TransactionKey = buildTransactionKey(txRequest) + console.log('\n\n', key, '\n\n', transactionRegistry, '\n\n') if (transactionRegistry) { - return transactionRegistry[key] + return transactionRegistry.get(key) } return undefined } @@ -57,35 +123,35 @@ export const transactionLookup = ( export const getTransactionIntentType = ({ methodId, txRequest, - contractRegistry, - transactionRegistry + contractRegistry }: { methodId: Hex txRequest: ValidatedInput contractRegistry?: ContractRegistry - transactionRegistry?: TransactionRegistry }): Intents => { - const trxStatus = transactionLookup(txRequest, transactionRegistry) - if (trxStatus === TransactionStatus.PENDING) { - return Intents.RETRY_TRANSACTION - } - if (trxStatus === TransactionStatus.FAILED) { - return Intents.CANCEL_TRANSACTION - } - if (methodId === NULL_METHOD_ID) { - return Intents.TRANSFER_NATIVE - } - if (methodId === SupportedMethodId.TRANSFER_FROM && contractRegistry) { - const assetType = contractTypeLookup(txRequest, contractRegistry) - if (assetType === AssetTypeEnum.ERC721) { - return Intents.TRANSFER_ERC721 - } - if (assetType === AssetTypeEnum.ERC20) { - return Intents.TRANSFER_ERC20 + const contractType = contractTypeLookup(txRequest, contractRegistry) + + // !! ORDER MATTERS !! + // Here we are checking for specific intents first. + // Then we check for intents tight to specific methods + // If nothing matches, we return the default Call Contract intent + const conditions = [ + // Transfer From condition + { + condition: () => + methodId === SupportedMethodId.TRANSFER_FROM && + contractType !== AssetTypeEnum.UNKNOWN && + contractType !== AssetTypeEnum.ERC1155 && + contractType !== AssetTypeEnum.NATIVE, + intent: contractType === AssetTypeEnum.ERC721 ? Intents.TRANSFER_ERC721 : Intents.TRANSFER_ERC20 + }, + // Supported methods conditions + { + condition: () => true, + intent: isSupportedMethodId(methodId) && SUPPORTED_METHODS[methodId].intent } - } - if (isSupportedMethodId(methodId) && SUPPORTED_METHODS[methodId].intent) { - return SUPPORTED_METHODS[methodId].intent || Intents.CALL_CONTRACT - } - return Intents.CALL_CONTRACT + ] + const { intent } = conditions.find(({ condition }) => condition())! + // default behavior: call contract + return intent || Intents.CALL_CONTRACT }