diff --git a/.github/actions/setup-forc/action.yml b/.github/actions/setup-forc/action.yml index ef40f39dd..76a77b84c 100644 --- a/.github/actions/setup-forc/action.yml +++ b/.github/actions/setup-forc/action.yml @@ -1,13 +1,13 @@ -name: "Rust & Forc Setup" +name: 'Rust & Forc Setup' inputs: rust-version: default: 1.81.0 forc-components: - default: "forc@0.66.4, fuel-core@0.40.0" + default: 'forc@0.68.1, fuel-core@0.45.1' runs: - using: "composite" + using: 'composite' steps: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ad66dd5aa..f54e26171 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,3 +6,4 @@ updates: schedule: interval: weekly open-pull-requests-limit: 10 + # Force re-evaluation diff --git a/.github/workflows/aws-deploy-api-hml.yml b/.github/workflows/aws-deploy-api-hml.yml deleted file mode 100644 index 84353cf2f..000000000 --- a/.github/workflows/aws-deploy-api-hml.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: "[API-HML] Deploy to Amazon ECS" - -on: - push: - branches: [homologacao] - -jobs: - check-changes: - name: "[VERIFY] Check for Changes" - runs-on: ubuntu-latest - - outputs: - should_deploy: ${{ steps.check.outputs.should_deploy }} - - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 2 - - - name: Check for changes in ./packages/api - id: check - run: | - if git diff --quiet HEAD~1 -- ./packages/api; then - echo "No changes detected in ./packages/api" - echo "::set-output name=should_deploy::false" - else - echo "Changes detected in ./packages/api" - echo "::set-output name=should_deploy::true" - fi - - deploy: - name: "[API-HMG] Deploy to Amazon ECS" - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.BAKO_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.BAKO_AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Login to Amazon ECR - id: login_ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Build, tag, and push image to Amazon ECR - id: build-image - env: - ECR_REGISTRY: ${{ steps.login_ecr.outputs.registry }} - ECR_REPOSITORY: bako-safe-api-hmg - IMAGE_TAG: ${{ github.sha }} - run: | - # Build a docker container and push it to ECR - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG ./packages/api - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - - - name: Fill in the new image ID in the Amazon ECS task definition - id: task-def - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: ./packages/api/hml_api_task_definition.json - container-name: bako-safe-api-hmg - image: ${{ steps.build-image.outputs.image }} - - - name: Deploy Amazon ECS task definition - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: ${{ steps.task-def.outputs.task-definition }} - service: bako-safe-api-hmg-service - cluster: Bako-Safe-Hmg - wait-for-service-stability: true diff --git a/.github/workflows/aws-deploy-api-stg.yml b/.github/workflows/aws-deploy-api-stg.yml deleted file mode 100644 index 085cb77d0..000000000 --- a/.github/workflows/aws-deploy-api-stg.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: "[API-STG] Deploy to Amazon ECS" - -on: - push: - branches: [staging] - -jobs: - check-changes: - name: "[VERIFY] Check for Changes" - runs-on: ubuntu-latest - - outputs: - should_deploy: ${{ steps.check.outputs.should_deploy }} - - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 2 - - - name: Check for changes in ./packages/api - id: check - run: | - if git diff --quiet HEAD~1 -- ./packages/api; then - echo "No changes detected in ./packages/api" - echo "::set-output name=should_deploy::false" - else - echo "Changes detected in ./packages/api" - echo "::set-output name=should_deploy::true" - fi - - deploy: - name: "[API-STG] Deploy to Amazon ECS" - needs: check-changes - runs-on: ubuntu-latest - if: needs.check-changes.outputs.should_deploy == 'true' - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.BAKO_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.BAKO_AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Login to Amazon ECR - id: login_ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Build, tag, and push image to Amazon ECR - id: build-image - env: - ECR_REGISTRY: ${{ steps.login_ecr.outputs.registry }} - ECR_REPOSITORY: bako-safe-api-stg - IMAGE_TAG: ${{ github.sha }} - run: | - # Build a docker container and push it to ECR - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG ./packages/api - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - - - name: Fill in the new image ID in the Amazon ECS task definition - id: task-def - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: ./packages/api/stg_api_task_definition.json - container-name: bako-safe-api-stg - image: ${{ steps.build-image.outputs.image }} - - - name: Deploy Amazon ECS task definition - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: ${{ steps.task-def.outputs.task-definition }} - service: bako-safe-api-stg-service - cluster: Bako-Safe-Stg - wait-for-service-stability: true diff --git a/.github/workflows/aws-deploy-api.yml b/.github/workflows/aws-deploy-api.yml index c5b5ec817..464665265 100644 --- a/.github/workflows/aws-deploy-api.yml +++ b/.github/workflows/aws-deploy-api.yml @@ -2,49 +2,62 @@ name: "[API] Deploy to Amazon ECS" on: push: - branches: [main] + branches: [main, homologacao] + paths: + - "packages/api/**" jobs: - check-changes: - name: "[VERIFY] Check for Changes" + setup: + name: "[SETUP] Determine Environment" runs-on: ubuntu-latest - outputs: - should_deploy: ${{ steps.check.outputs.should_deploy }} + environment: ${{ steps.env.outputs.environment }} + ecr_repository: ${{ steps.env.outputs.ecr_repository }} + ecs_service: ${{ steps.env.outputs.ecs_service }} + ecs_cluster: ${{ steps.env.outputs.ecs_cluster }} + task_definition: ${{ steps.env.outputs.task_definition }} + container_name: ${{ steps.env.outputs.container_name }} steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 2 - - - name: Check for changes in ./packages/api - id: check + - name: Determine environment from branch + id: env run: | - if git diff --quiet HEAD~1 -- ./packages/api; then - echo "No changes detected in ./packages/api" - echo "::set-output name=should_deploy::false" - else - echo "Changes detected in ./packages/api" - echo "::set-output name=should_deploy::true" + BRANCH="${GITHUB_REF#refs/heads/}" + echo "Branch: $BRANCH" + + if [ "$BRANCH" = "main" ]; then + echo "environment=production" >> $GITHUB_OUTPUT + echo "ecr_repository=bako-safe-api" >> $GITHUB_OUTPUT + echo "ecs_service=bako-safe-api-service" >> $GITHUB_OUTPUT + echo "ecs_cluster=Bako-Safe-ECS" >> $GITHUB_OUTPUT + echo "task_definition=./packages/api/prod_api_task_definition.json" >> $GITHUB_OUTPUT + echo "container_name=bako-safe-api" >> $GITHUB_OUTPUT + elif [ "$BRANCH" = "homologacao" ]; then + echo "environment=homologacao" >> $GITHUB_OUTPUT + echo "ecr_repository=bako-safe-api-hmg" >> $GITHUB_OUTPUT + echo "ecs_service=bako-safe-api-hmg-service" >> $GITHUB_OUTPUT + echo "ecs_cluster=Bako-Safe-Hmg" >> $GITHUB_OUTPUT + echo "task_definition=./packages/api/hml_api_task_definition.json" >> $GITHUB_OUTPUT + echo "container_name=bako-safe-api-hmg" >> $GITHUB_OUTPUT fi deploy: - name: "[API] Deploy to Amazon ECS" + name: "[API] Deploy to ${{ needs.setup.outputs.environment }}" + needs: setup runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.BAKO_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.BAKO_AWS_SECRET_ACCESS_KEY }} @@ -52,32 +65,31 @@ jobs: - name: Login to Amazon ECR id: login_ecr - uses: aws-actions/amazon-ecr-login@v1 + uses: aws-actions/amazon-ecr-login@v2 - name: Build, tag, and push image to Amazon ECR id: build-image - env: - ECR_REGISTRY: ${{ steps.login_ecr.outputs.registry }} - ECR_REPOSITORY: bako-safe-api - IMAGE_TAG: ${{ github.sha }} - run: | - # Build a docker container and push it to ECR - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG ./packages/api - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" + uses: docker/build-push-action@v6 + with: + context: ./packages/api + platforms: linux/arm64 + push: true + tags: ${{ steps.login_ecr.outputs.registry }}/${{ needs.setup.outputs.ecr_repository }}:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max - name: Fill in the new image ID in the Amazon ECS task definition id: task-def uses: aws-actions/amazon-ecs-render-task-definition@v1 with: - task-definition: ./packages/api/prod_api_task_definition.json - container-name: bako-safe-api - image: ${{ steps.build-image.outputs.image }} + task-definition: ${{ needs.setup.outputs.task_definition }} + container-name: ${{ needs.setup.outputs.container_name }} + image: ${{ steps.login_ecr.outputs.registry }}/${{ needs.setup.outputs.ecr_repository }}:${{ github.sha }} - name: Deploy Amazon ECS task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: task-definition: ${{ steps.task-def.outputs.task-definition }} - service: bako-safe-api-service - cluster: Bako-Safe-ECS + service: ${{ needs.setup.outputs.ecs_service }} + cluster: ${{ needs.setup.outputs.ecs_cluster }} wait-for-service-stability: true diff --git a/.github/workflows/aws-deploy-socket-stg.yml b/.github/workflows/aws-deploy-socket-stg.yml deleted file mode 100644 index aab823f3d..000000000 --- a/.github/workflows/aws-deploy-socket-stg.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: "[Socket Server STG] Deploy to Amazon ECS" - -on: - push: - branches: [staging] - -jobs: - check-changes: - name: "[VERIFY] Check for Changes" - runs-on: ubuntu-latest - - outputs: - should_deploy: ${{ steps.check.outputs.should_deploy }} - - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 2 - - - name: Check for changes in ./packages/socket-server - id: check - run: | - if git diff --quiet HEAD~1 -- ./packages/socket-server; then - echo "No changes detected in ./packages/socket-server" - echo "::set-output name=should_deploy::false" - else - echo "Changes detected in ./packages/socket-server" - echo "::set-output name=should_deploy::true" - fi - - deploy: - name: "[Socket Server STG] Deploy to Amazon ECS" - needs: check-changes - runs-on: ubuntu-latest - if: needs.check-changes.outputs.should_deploy == 'true' - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.BAKO_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.BAKO_AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Login to Amazon ECR - id: login_ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Build, tag, and push image to Amazon ECR - id: build-image - env: - ECR_REGISTRY: ${{ steps.login_ecr.outputs.registry }} - ECR_REPOSITORY: bako-safe-socket-server-api-stg - IMAGE_TAG: ${{ github.sha }} - run: | - # Build a docker container and push it to ECR - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG ./packages/socket-server - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - - - name: Fill in the new image ID in the Amazon ECS task definition - id: task-def - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: ./packages/socket-server/stg_socket_task_definition.json - container-name: bako-safe-socket-server-api-stg - image: ${{ steps.build-image.outputs.image }} - - - name: Deploy Amazon ECS task definition - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: ${{ steps.task-def.outputs.task-definition }} - service: bako-safe-socket-server-api-stg-service - cluster: Bako-Safe-Stg - wait-for-service-stability: true diff --git a/.github/workflows/aws-deploy-worker.yml b/.github/workflows/aws-deploy-worker.yml deleted file mode 100644 index f8f11050a..000000000 --- a/.github/workflows/aws-deploy-worker.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: "[WORKER] Deploy to Amazon ECS" - -on: - push: - branches: [staging] - -jobs: - check-changes: - name: "[VERIFY] Check for Changes" - runs-on: ubuntu-latest - - outputs: - should_deploy: ${{ steps.check.outputs.should_deploy }} - - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 2 - - - name: Check for changes in ./packages/worker - id: check - run: | - if git diff --quiet HEAD~1 -- ./packages/worker; then - echo "No changes detected in ./packages/worker" - echo "::set-output name=should_deploy::false" - else - echo "Changes detected in ./packages/worker" - echo "::set-output name=should_deploy::true" - fi - - deploy: - name: "[WORKER] Deploy to Amazon ECS" - needs: check-changes - runs-on: ubuntu-latest - if: needs.check-changes.outputs.should_deploy == 'true' - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.BAKO_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.BAKO_AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Login to Amazon ECR - id: login_ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Build, tag, and push image to Amazon ECR - id: build-image - env: - ECR_REGISTRY: ${{ steps.login_ecr.outputs.registry }} - ECR_REPOSITORY: bako-safe-worker - IMAGE_TAG: ${{ github.sha }} - run: | - # Build a docker container and push it to ECR - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG ./packages/worker - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - - - name: Fill in the new image ID in the Amazon ECS task definition - id: task-def - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: ./packages/worker/worker_task_def.json - container-name: bako-safe-worker - image: ${{ steps.build-image.outputs.image }} - - - name: Deploy Amazon ECS task definition - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: ${{ steps.task-def.outputs.task-definition }} - service: bako-safe-worker-service - cluster: Bako-Safe-ECS - wait-for-service-stability: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..707f636fb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,254 @@ +name: CI + +on: + workflow_dispatch: + push: + branches: + - main + - staging + paths: + - 'packages/api/**' + - 'packages/socket-server/**' + - 'packages/worker/**' + - '.github/workflows/ci.yml' + pull_request: + branches: + - "**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + changelog: + name: Changelog + runs-on: ubuntu-latest + timeout-minutes: 2 + if: github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check CHANGELOG.md was updated + run: | + if git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -q "^CHANGELOG.md$"; then + echo "✅ CHANGELOG.md was updated" + else + echo "❌ CHANGELOG.md was NOT updated" + echo "" + echo "Please update CHANGELOG.md with your changes." + echo "Follow the Keep a Changelog format: https://keepachangelog.com/" + exit 1 + fi + + lint: + name: Lint + runs-on: ubuntu-latest + timeout-minutes: 5 + + defaults: + run: + working-directory: packages/api + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Run ESLint + run: pnpm lint + + typecheck: + name: Typecheck + runs-on: ubuntu-latest + timeout-minutes: 5 + + defaults: + run: + working-directory: packages/api + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Run TypeScript check + run: pnpm typecheck + + security: + name: Security Audit + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.actor != 'dependabot[bot]' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Run security audit + run: pnpm audit --audit-level=high + + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 10 + + defaults: + run: + working-directory: packages/api + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-forc + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Build + run: pnpm build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build + path: packages/api/build + retention-days: 1 + + docker-build: + name: Docker Build (${{ matrix.package }}) + runs-on: ubuntu-latest + timeout-minutes: 15 + + strategy: + fail-fast: false + matrix: + include: + - package: api + context: packages/api + platform: linux/amd64 + - package: socket-server + context: packages/socket-server + platform: linux/amd64 + - package: worker + context: packages/worker + platform: linux/amd64 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: ${{ matrix.context }} + platforms: ${{ matrix.platform }} + push: false + + test: + name: Tests + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: build + + defaults: + run: + working-directory: packages/api + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: ./.github/actions/setup-forc + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build + path: packages/api/build + + - name: Copy predicate releases + run: pnpm copy:predicate-releases + + - name: Copy .env.test to .env + run: cp .env.test .env + + - name: Run tests + run: node --test-force-exit --test build/tests/*.tests.js + env: + TESTCONTAINERS_DB: "true" diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 000000000..c809623ad --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,193 @@ +name: "[Staging] Deploy to Amazon ECS" + +on: + push: + branches: [staging] + workflow_dispatch: + +concurrency: + group: deploy-staging + cancel-in-progress: false + +jobs: + detect-changes: + name: "[DETECT] Changed Packages" + runs-on: ubuntu-latest + outputs: + api: ${{ steps.filter.outputs.api }} + socket-server: ${{ steps.filter.outputs.socket-server }} + worker: ${{ steps.filter.outputs.worker }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect changed packages + uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + api: + - 'packages/api/**' + socket-server: + - 'packages/socket-server/**' + worker: + - 'packages/worker/**' + + deploy-api: + name: "[API] Deploy to Staging" + needs: detect-changes + if: needs.detect-changes.outputs.api == 'true' + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.BAKO_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.BAKO_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Login to Amazon ECR + id: login_ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag, and push image to Amazon ECR + uses: docker/build-push-action@v6 + with: + context: ./packages/api + platforms: linux/arm64 + push: true + tags: ${{ steps.login_ecr.outputs.registry }}/bako-safe-api-stg:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: ./packages/api/stg_api_task_definition.json + container-name: bako-safe-api-stg + image: ${{ steps.login_ecr.outputs.registry }}/bako-safe-api-stg:${{ github.sha }} + + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: bako-safe-api-stg-service + cluster: Bako-Safe-Stg + wait-for-service-stability: true + + deploy-socket-server: + name: "[Socket Server] Deploy to Staging" + needs: detect-changes + if: needs.detect-changes.outputs.socket-server == 'true' + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.BAKO_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.BAKO_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Login to Amazon ECR + id: login_ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag, and push image to Amazon ECR + uses: docker/build-push-action@v6 + with: + context: ./packages/socket-server + platforms: linux/arm64 + push: true + tags: ${{ steps.login_ecr.outputs.registry }}/bako-safe-socket-server-api-stg:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: ./packages/socket-server/stg_socket_task_definition.json + container-name: bako-safe-socket-server-api-stg + image: ${{ steps.login_ecr.outputs.registry }}/bako-safe-socket-server-api-stg:${{ github.sha }} + + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: bako-safe-socket-server-api-stg-service + cluster: Bako-Safe-Stg + wait-for-service-stability: true + + deploy-worker: + name: "[Worker] Deploy to Staging" + needs: detect-changes + if: needs.detect-changes.outputs.worker == 'true' + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.BAKO_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.BAKO_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Login to Amazon ECR + id: login_ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag, and push image to Amazon ECR + uses: docker/build-push-action@v6 + with: + context: ./packages/worker + platforms: linux/arm64 + push: true + tags: ${{ steps.login_ecr.outputs.registry }}/bako-safe-worker:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: ./packages/worker/worker_task_def.json + container-name: bako-safe-worker + image: ${{ steps.login_ecr.outputs.registry }}/bako-safe-worker:${{ github.sha }} + + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: bako-safe-worker-service + cluster: Bako-Safe-ECS + wait-for-service-stability: true diff --git a/.github/workflows/test-api.yml b/.github/workflows/test-api.yml deleted file mode 100644 index 164cc08d8..000000000 --- a/.github/workflows/test-api.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Run API Tests - -on: - pull_request: - branches: - - "**" - -jobs: - test: - runs-on: ubuntu-latest - - defaults: - run: - working-directory: packages/api - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: ./.github/actions/setup-forc - - - name: Install pnpm - run: npm install -g pnpm - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: "pnpm" - - - name: Install dependencies - run: pnpm install --no-frozen-lockfile - - - name: Copy .env.test to .env - run: cp .env.test .env - - - name: Build and run tests - run: pnpm test:build - env: - TESTCONTAINERS_DB: "true" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..e20ca9534 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Comprehensive CI pipeline with lint, typecheck, security audit, and tests +- Test coverage for workspace and connections modules +- Test stability analysis documentation +- Added environment variable `SOCKET_CLIENT_DISCONNECT_TIMEOUT` to allow configuration of the socket client's auto-disconnect timeout. +- Health check endpoints for API uptime monitoring: + - `GET /healthcheck/db` - PostgreSQL connectivity check (executes `SELECT 1`) + - `GET /healthcheck/redis` - Redis connectivity check (executes `PING` on both read/write clients) + - All endpoints return HTTP 200 with `{ status: 'ok' }` on success or HTTP 5\*\* on failure + - Designed for integration with uptime monitoring services (e.g., UptimeRobot, Datadog, New Relic) + +### Changed + +- Improved README with complete setup instructions +- Optimized CI workflow with caching and concurrency +- Removed Docker-based start command for socket-server in development; now runs directly with Node.js for local development +– Additional logs for detailed tracking of socket events emitted on the API +- Increased the socket auto-disconnect timeout after event emission to ensure more reliable delivery and prevent premature disconnections during high-latency operations. +- Updated the `build:prod` script to execute `postbuild` after the build process, ensuring all necessary post-build steps are consistently applied in production builds. +- Method `findById` of PredicateService now returns the `email` and `notify` fields of predicate members. + +### Fixed + +- Security vulnerabilities in dependencies (js-yaml, uglify-js) +- Test infrastructure funding amount for fuel-core compatibility +- CLI token tests now environment-independent +- Worker deploy failing due to arm64v8-specific Docker images on amd64 runner +- Worker deploy workflow modernized to use docker/build-push-action with buildx +- Email sending errors during predicate creation no longer interrupt the creation flow; failures are logged but do not block predicate creation. + +### Security + +- Added pnpm overrides for vulnerable dependencies +- Security audit job in CI pipeline +- Upgraded axios from 1.12.0 to 1.13.5 (GHSA-43fc-jf86-j433) +- Upgraded nodemailer to 8.0.1 to resolve DoS vulnerability (GHSA-rcmh-qjqh-p98v) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..fa02e2b03 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,95 @@ +# Contributing to Bako Safe API + +Thank you for your interest in contributing to Bako Safe! + +## Getting Started + +1. Fork the repository +2. Clone your fork +3. Follow the [Development Setup](README.md#development) instructions + +## Development Workflow + +### Branch Naming + +Use descriptive branch names: +- `feature/add-new-endpoint` +- `fix/transaction-validation` +- `docs/update-readme` +- `refactor/auth-module` + +### Code Style + +This project uses ESLint and Prettier for code formatting: + +```bash +# Check formatting +pnpm lint + +# Fix formatting issues +pnpm lint --fix +``` + +### Commit Messages + +Follow [Conventional Commits](https://www.conventionalcommits.org/): + +``` +type(scope): description + +feat: add new endpoint for workspace settings +fix: resolve transaction timeout issue +docs: update API documentation +refactor: simplify auth middleware +test: add tests for predicate module +chore: update dependencies +``` + +### Testing + +Before submitting a PR: + +```bash +# Run tests +cd packages/api && pnpm test:build + +# Verify the build +cd packages/api && pnpm build +``` + +## Pull Request Process + +1. Update documentation if needed +2. Ensure all tests pass +3. Fill out the PR template +4. Request review from maintainers + +### PR Template + +When creating a PR, include: +- Brief description of changes +- Summary of what was done +- Link to related issue (if applicable) + +## Project Structure + +``` +packages/ +├── api/ # Main REST API +├── socket-server/ # WebSocket server +├── database/ # Database Docker setup +├── redis/ # Redis Docker setup +├── chain/ # Local Fuel network +├── worker/ # Background jobs +└── metabase/ # Analytics +``` + +## Need Help? + +- Check existing issues +- Create a new issue with details +- Join our community channels + +## License + +By contributing, you agree that your contributions will be licensed under the Apache-2.0 License. diff --git a/DOCS_REVIEW.md b/DOCS_REVIEW.md new file mode 100644 index 000000000..e2a3e8880 --- /dev/null +++ b/DOCS_REVIEW.md @@ -0,0 +1,465 @@ +# Revisão de Documentação e Setup - bako-safe-api + +> Documento gerado durante análise de onboarding de novo desenvolvedor. +> Branch: `staging-docs-review` +> Data: 2026-02-05 + +--- + +## Status da Execução + +| Etapa | Status | Observações | +|-------|--------|-------------| +| Clone do repositório | ✅ OK | - | +| Checkout da branch | ✅ OK | - | +| pnpm install | ✅ OK | Warning sobre `resolutions` no package.json da API | +| Copiar .env files | ✅ OK | - | +| Criar docker network | ✅ OK | - | +| pnpm dev (Quick Start) | ❌ FALHA | Race condition + variáveis faltando | +| Manual Setup | ✅ OK | Funciona seguindo passo a passo | +| Migrations | ❌ FALHA | Script aponta para path inexistente | +| Testes | ⚠️ PARCIAL | 33/35 passaram, 2 falharam (cleanup assíncrono) | + +### Conclusão do Onboarding + +**Tempo gasto:** ~30 minutos (deveria ser ~5 minutos) + +**Bloqueadores encontrados:** +1. Quick Start (`pnpm dev`) não funciona out-of-the-box +2. `.env.example` incompleto - faltam variáveis críticas +3. `RIG_ID_CONTRACT` vazio causa crash da API +4. Script de migration quebrado + +**O que funcionou bem:** +- Testcontainers para testes (excelente DX) +- Setup manual com Docker funciona +- Estrutura de packages clara + +--- + +## Problemas Críticos Encontrados + +### 1. Race Condition na Inicialização (CRÍTICO) + +**Problema:** O comando `pnpm dev` falha porque o Turbo inicia todos os serviços em paralelo. A API e o Socket-Server tentam conectar ao banco de dados antes dele estar healthy. + +**Erro observado:** +``` +bakosafe-api:dev: Error: connect ECONNREFUSED 127.0.0.1:5432 +bakosafe-socket-server:dev: Error: getaddrinfo ENOTFOUND db +``` + +**Causa raiz:** O `turbo.json` define dependências entre tasks, mas as tasks de infraestrutura (db, redis, chain) não bloqueiam adequadamente as tasks de aplicação. + +**Impacto:** Desenvolvedor não consegue usar o Quick Start documentado. + +**Sugestão de correção:** +- Opção A: Adicionar script de wait-for-it antes de iniciar API/Socket +- Opção B: Documentar que deve-se usar o Manual Setup +- Opção C: Separar `pnpm dev:infra` de `pnpm dev:app` + +--- + +### 2. Socket-Server .env.example com HOST Incorreto (CRÍTICO) + +**Arquivo:** `packages/socket-server/.env.example` + +**Problema:** +```env +DATABASE_HOST=db # Este é o hostname Docker interno! +``` + +**Deveria ser:** +```env +DATABASE_HOST=127.0.0.1 # Para desenvolvimento local fora do Docker +``` + +**Impacto:** Socket-Server não inicia em desenvolvimento local. + +--- + +### 3. UI_URL Inconsistente Entre Packages (MÉDIO) + +| Package | UI_URL | +|---------|--------| +| api | `http://localhost:5175` | +| socket-server | `http://localhost:5173` | + +**Impacto:** Confusão sobre qual porta o frontend deve rodar. + +--- + +## Gaps de Documentação + +### README.md Principal + +| Item | Status | Prioridade | +|------|--------|------------| +| Visão geral do projeto (o que é Bako Safe?) | ❌ Ausente | Alta | +| Arquitetura do sistema | ❌ Ausente | Alta | +| Diagrama de componentes | ❌ Ausente | Média | +| Descrição de cada package | ❌ Ausente | Alta | +| Como rodar migrations | ❌ Ausente | Alta | +| Configuração de Redis para API | ❌ Ausente | Alta | +| Como contribuir (CONTRIBUTING.md) | ❌ Ausente | Média | +| Troubleshooting expandido | ⚠️ Parcial | Média | + +### Variáveis de Ambiente Não Documentadas + +**packages/api/.env.example** - Variáveis sem explicação: +- `API_TOKEN_SECRET` / `API_TOKEN_SECRET_IV` - Para que servem? +- `API_SOCKET_SESSION_ID` - Valor hardcoded, é seguro? +- `FUEL_PROVIDER_CHAIN_ID` - Quando usar 0 vs 9889? +- `RIG_ID_CONTRACT` - Obrigatório? Onde obter? +- `DB_METABASE_USERNAME` / `DB_METABASE_PASS` - São necessários para dev? +- `COIN_MARKET_CAP_API_KEY` - Obrigatório? Funciona sem? + +### Packages Sem Documentação + +| Package | README | Descrição | +|---------|--------|-----------| +| api | ❌ Não | Apenas README de contracts/rig | +| chain | ❌ Não | Nenhuma doc | +| database | ❌ Não | Nenhuma doc | +| redis | ❌ Não | Nenhuma doc | +| socket-server | ❌ Não | Nenhuma doc | +| metabase | ❌ Não | Nenhuma doc | +| worker | ✅ Sim | Tem README completo | + +--- + +## Inconsistências no Código + +### 1. Variáveis Duplicadas em api/.env.example + +```env +ASSETS_URL=https://besafe-asset.s3.amazonaws.com/icon +ASSETS_URL=https://besafe-asset.s3.amazonaws.com/icon # DUPLICADO + +APP_ADMIN_EMAIL=admin_user_email +# ... +APP_ADMIN_EMAIL=admin_user_email # DUPLICADO +APP_ADMIN_PASSWORD=admin_user_password # DUPLICADO +``` + +### 2. Typo em worker/.env.example + +```env +WORKER_MONGO_ENVIRONMENT=devevelopment # Typo: "devevelopment" +``` + +### 3. Worker README Desatualizado + +O README menciona `pnpm worker:dev:start` mas esse script não existe no package.json do worker. + +--- + +## Documentação de API (Swagger/OpenAPI) + +**Status:** ❌ Inexistente + +**Endpoints identificados (sem documentação):** +- `/auth/*` - Autenticação +- `/user/*` - Usuários +- `/cli/*` - CLI Auth +- `/connections/*` - dApps +- `/api-token/*` - API Tokens +- `/workspace/*` - Workspaces +- `/predicate/*` - Predicates +- `/address-book/*` - Address Book +- `/transaction/*` - Transações +- `/notifications/*` - Notificações +- `/external/*` - Rotas externas +- `/ping` - Health check simples +- `/healthcheck` - Health check + +--- + +## Log de Execução + +### Tentativa 1: Quick Start (pnpm dev) + +```bash +$ pnpm install +# ✅ OK - 1315 packages instalados + +$ cp packages/api/.env.example packages/api/.env +$ cp packages/database/.env.example packages/database/.env +$ cp packages/redis/.env.example packages/redis/.env +$ cp packages/socket-server/.env.example packages/socket-server/.env +# ✅ OK + +$ docker network create bako-network +# ✅ OK + +$ pnpm dev +# ❌ FALHA +# - Redis: ✅ Healthy +# - Database: ✅ Healthy (após ~12s) +# - MongoDB: ✅ Healthy +# - Fuel Chain: ✅ Healthy +# - Socket-Server: ❌ Error: getaddrinfo ENOTFOUND db +# - API: ❌ Error: connect ECONNREFUSED 127.0.0.1:5432 +``` + +**Conclusão:** O Quick Start não funciona out-of-the-box. + +--- + +### Tentativa 2: Manual Setup + +```bash +# 1. Database +$ cd packages/database && docker compose --env-file .env.example up -d +# ✅ OK - postgres e mongodb healthy + +# 2. Redis +$ cd packages/redis && docker compose --env-file .env.example up -d +# ✅ OK - redis healthy + +# 3. Fuel Chain +$ cd packages/chain && docker compose -p bako-safe_dev --env-file .env.chain up -d --build +# ✅ OK - fuel-core e faucet rodando + +# 4. Socket Server +$ cd packages/socket-server && docker compose up -d --build +# ✅ OK - socket-server healthy + +# 5. API +$ cd packages/api && pnpm dev +# ❌ FALHA - Erro: FuelError: Unknown address format +``` + +**Erro na API:** +``` +FuelError: Unknown address format: only 'B256', 'Public Key (512)', or 'EVM Address' are supported. + at new Rig (/packages/api/src/contracts/rig/mainnet/types/Rig.ts:1645:5) + at Function.start (/packages/api/src/server/storage/rig.ts:35:19) +``` + +**Causa:** `RIG_ID_CONTRACT` está vazio no `.env.example` + +--- + +## Análise do .env Completo vs .env.example + +Comparando o arquivo de ambiente funcional com o `.env.example`: + +### Variáveis Faltando no .env.example (CRÍTICO) + +| Variável | Valor Exemplo | Descrição | +|----------|---------------|-----------| +| `REDIS_URL_WRITE` | `redis://localhost:6379` | URL do Redis para escrita | +| `REDIS_URL_READ` | `redis://localhost:6379` | URL do Redis para leitura | +| `WORKER_URL` | `http://localhost:3063` | URL do Worker | +| `MELD_SANDBOX_API_KEY` | `***` | API Key do MELD (sandbox) | +| `MELD_SANDBOX_API_URL` | `https://api-sb.meld.io/` | URL API MELD sandbox | +| `MELD_SANDBOX_WEBHOOK_SECRET` | `***` | Webhook secret MELD | +| `MELD_PRODUCTION_API_KEY` | `***` | API Key MELD produção | +| `MELD_PRODUCTION_API_URL` | `https://api.meld.io/` | URL API MELD produção | +| `MELD_PRODUCTION_WEBHOOK_SECRET` | `***` | Webhook secret MELD prod | +| `LAYERS_SWAP_API_URL` | `https://api.layerswap.io/api/v2` | URL LayerSwap | +| `LAYERS_SWAP_API_KEY_SANDBOX` | `***` | API Key LayerSwap sandbox | +| `LAYERS_SWAP_API_KEY_PROD` | `***` | API Key LayerSwap prod | +| `LAYERS_SWAP_WEBHOOK_SECRET` | `***` | Webhook LayerSwap | +| `ENABLE_BALANCE_CACHE` | `true` | Habilita cache de balance | +| `BALANCE_CACHE_TTL` | `300` | TTL do cache de balance | +| `BALANCE_INVALIDATION_TTL` | `3600` | TTL invalidação cache | +| `WARMUP_ENABLED` | `true` | Habilita warmup | +| `WARMUP_CONCURRENCY` | `5` | Concorrência warmup | +| `WARMUP_MAX_PREDICATES` | `20` | Max predicates warmup | +| `WARMUP_SKIP_CACHED` | `true` | Pula cached no warmup | +| `TRANSACTION_CACHE_TTL` | `600` | TTL cache transações | +| `TRANSACTION_INCREMENTAL_LIMIT` | `10` | Limite incremental | +| `INTERNAL_API_KEY` | `worker_api_key` | Chave interna para Worker | +| `NODE_ENV` | `development` | Ambiente Node | + +### Valores Incorretos no .env.example + +| Variável | .env.example | Valor Correto | +|----------|--------------|---------------| +| `FUEL_PROVIDER` | `http://127.0.0.1:4000/v1/graphql` | OK para local, mas falta opção testnet | +| `UI_URL` | `http://localhost:5175` | `http://localhost:5174` (inconsistente) | +| `RIG_ID_CONTRACT` | *(vazio)* | `0x2181f1b8e00756672515807cab7de10c70a9b472a4a9b1b6ca921435b0a1f49b` | + +### Sugestão: RIG_ID_CONTRACT como Constante + +O `RIG_ID_CONTRACT` é um endereço de contrato público na mainnet. Sugestão: + +```typescript +// src/constants/contracts.ts +export const RIG_CONTRACTS = { + MAINNET: '0x2181f1b8e00756672515807cab7de10c70a9b472a4a9b1b6ca921435b0a1f49b', + TESTNET: null, // não existe em testnet +} as const; +``` + +E no código usar fallback: +```typescript +const rigAddress = RIG_ID_CONTRACT || RIG_CONTRACTS.MAINNET; +``` + +Ou melhor ainda - tornar o RigInstance opcional em dev: +```typescript +if (RIG_ID_CONTRACT) { + this.rigCache = RigInstance.start(); +} +``` + +--- + +--- + +### Tentativa 3: Rodar Migrations + +```bash +$ cd packages/api && pnpm migration:run +# ❌ FALHA +``` + +**Erro:** +``` +Error: Unable to open file: "/packages/api/src/database" +Cannot find module '/packages/api/src/database' +``` + +**Causa:** O script `migration:run` no package.json aponta para `src/database` que é um **diretório**, não um arquivo: + +```json +"migration:run": "ts-node ... --dataSource src/database" +``` + +**Problema:** Não existe um arquivo `dataSource.ts` exportando o DataSource do TypeORM. A configuração real está em: +- `src/config/database.ts` - Função `getDatabaseConfig()` +- `src/config/connection.ts` - Função `getDatabaseInstance()` + +**Sugestão:** Criar arquivo `src/database/index.ts`: +```typescript +import { DataSource } from 'typeorm'; +import { getDatabaseConfig } from '../config/database'; + +export default new DataSource(getDatabaseConfig()); +``` + +Ou corrigir o script para: +```json +"migration:run": "ts-node ... --dataSource src/config/connection" +``` + +--- + +### Tentativa 4: Rodar Testes + +```bash +$ cd packages/api && pnpm test:build +``` + +**Resultado:** ⚠️ PARCIAL +- Total: 35 testes +- Passou: 33 +- Falhou: 2 + +**Erros encontrados:** +1. `build/tests/predicate.tests.js` - Falhou +2. `build/tests/user.tests.js` - Falhou com: + ``` + generated asynchronous activity after the test ended. + Error: App is not started + ``` + +**Análise:** Os erros parecem ser de cleanup assíncrono após os testes, não falhas funcionais. + +**Nota:** Os testes usam `testcontainers` que inicia um PostgreSQL automaticamente - isso é bem documentado e funciona. + +--- + +## Checklist de Correções Sugeridas + +### Prioridade 0 (Bloqueadores) + +- [x] Corrigir `DATABASE_HOST` em `packages/socket-server/.env.example` para `127.0.0.1` +- [x] Adicionar mecanismo de retry/wait na inicialização da API e Socket-Server +- [x] Corrigir script `migration:run` - aponta para diretório inexistente (criado database/index.ts) +- [x] Adicionar variáveis de Redis faltando no `.env.example` (`REDIS_URL_WRITE`, `REDIS_URL_READ`) +- [x] Adicionar `RIG_ID_CONTRACT` no `.env.example` ou tornar opcional em dev +- [x] Corrigir race condition no `pnpm dev` (wait-on + healthchecks) +- [x] Corrigir socket-server database config para aceitar 'postgres' como host local +- [x] Atualizar Makefiles para Docker Compose V2 syntax + +### Prioridade 1 (Essenciais) + +- [x] Adicionar seção "O que é Bako Safe?" no README +- [x] Documentar como rodar migrations +- [x] Documentar arquitetura dos packages +- [x] Unificar `UI_URL` entre packages (5173 vs 5175) -> 5174 +- [x] Adicionar configuração de Redis no `.env.example` da API +- [ ] Criar documentação Swagger/OpenAPI + +### Prioridade 2 (Melhorias) + +- [x] Remover variáveis duplicadas dos `.env.example` +- [x] Corrigir typo `devevelopment` no worker +- [x] Criar CONTRIBUTING.md +- [x] Adicionar diagrama de arquitetura (texto no README) +- [x] Atualizar README do worker com scripts corretos (já estava correto) + +--- + +## Próximos Passos + +1. ~~Tentar setup manual (passo a passo)~~ ✅ +2. ~~Testar migrations~~ ❌ Script quebrado +3. ~~Rodar testes~~ ⚠️ 33/35 passaram +4. Documentar fluxo completo funcional + +--- + +## Setup Manual Funcional (Testado) + +Para desenvolvedores novos, este é o fluxo que **realmente funciona**: + +```bash +# 1. Clone e setup inicial +git clone https://github.com/infinitybase/bako-safe-api.git +cd bako-safe-api +git checkout staging-docs-review +pnpm install + +# 2. Criar rede Docker +docker network create bako-network + +# 3. Copiar e configurar .env +cp packages/api/.env.example packages/api/.env +cp packages/database/.env.example packages/database/.env +cp packages/redis/.env.example packages/redis/.env +cp packages/socket-server/.env.example packages/socket-server/.env + +# IMPORTANTE: Editar packages/api/.env e adicionar: +# - REDIS_URL_WRITE=redis://localhost:6379 +# - REDIS_URL_READ=redis://localhost:6379 +# - RIG_ID_CONTRACT=0x2181f1b8e00756672515807cab7de10c70a9b472a4a9b1b6ca921435b0a1f49b + +# 4. Subir infraestrutura (em ordem!) +cd packages/database && docker compose --env-file .env.example up -d +# Aguardar containers ficarem healthy (~15s) +cd ../redis && docker compose --env-file .env.example up -d +cd ../chain && docker compose -p bako-safe_dev --env-file .env.chain up -d --build +cd ../socket-server && docker compose up -d --build + +# 5. Verificar todos os containers +docker ps +# Deve mostrar: postgres, mongodb-dev, redis-bako-dev, bakosafe_fuel-core, bakosafe_faucet, bako-socket-server + +# 6. Iniciar API +cd ../api && pnpm dev + +# 7. Testar +curl http://localhost:3333/ping +curl http://localhost:3333/healthcheck +``` + +### Para rodar testes (sem setup manual): +```bash +cd packages/api && pnpm test:build +# Usa testcontainers - não precisa de Docker rodando antes +``` diff --git a/README.md b/README.md index bf819a49e..c46f3d862 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,229 @@ # Bako Safe API -### Development +Bako Safe is a multisig wallet solution built on the [Fuel Network](https://fuel.network/). This repository contains the backend API and supporting services for the Bako Safe ecosystem. -1. Install [Docker](https://docs.docker.com/engine/install/) -2. Install [PNPM](https://pnpm.io/installation#using-npm): `npm install -g pnpm` -3. Install dependencies: `pnpm install` -4. Run the api in the root folder: `pnpm dev` +## Architecture -### Tests +``` +bako-safe-api/ +├── packages/ +│ ├── api/ # Main REST API (Express + TypeORM) +│ ├── socket-server/ # WebSocket server for real-time events +│ ├── database/ # PostgreSQL + MongoDB Docker setup +│ ├── redis/ # Redis cache Docker setup +│ ├── chain/ # Local Fuel network (fuel-core + faucet) +│ ├── worker/ # Background jobs (Bull + Redis) +│ └── metabase/ # Analytics dashboard +``` -1. Install [Docker](https://docs.docker.com/engine/install/) -2. Install [PNPM](https://pnpm.io/installation#using-npm): `npm install -g pnpm` -3. Install dependencies: `pnpm install` -4. Run the api in the root folder: `pnpm dev` -5. In new terminal, run the tests: `cd packages/api && pnpm test` +## Requirements -### Database utilities: +- [Docker](https://docs.docker.com/engine/install/) (v20.10+ with Docker Compose V2) +- [PNPM](https://pnpm.io/installation#using-npm): `npm install -g pnpm` +- Node.js 20+ -#### Populate DB +## Development -1. Copy your scripts to insert infos to path `packages/api/database/inserts` -2. Run script `cd packages/api && pnpm migration:populate` +### Quick Start +1. Install dependencies: + ```bash + pnpm install + ``` + +2. Copy environment files: + ```bash + cp packages/api/.env.example packages/api/.env + cp packages/database/.env.example packages/database/.env + cp packages/redis/.env.example packages/redis/.env + cp packages/socket-server/.env.example packages/socket-server/.env + ``` + +3. Run the API (network is created automatically): + ```bash + pnpm dev + ``` + +4. Verify everything is running: + ```bash + curl http://localhost:3333/ping + curl http://localhost:3333/healthcheck + docker ps # Should show 6 healthy containers + ``` + +### Manual Setup (Step by Step) + +If you need more control, start services individually: + +1. Create Docker network: + ```bash + docker network create bako-network + ``` + +2. Start database: + ```bash + cd packages/database && docker compose --env-file .env.example up -d + # Wait for healthy: docker ps | grep postgres + ``` + +3. Start Redis: + ```bash + cd packages/redis && docker compose --env-file .env.example up -d + ``` + +4. Start Fuel Chain (local network): + ```bash + cd packages/chain && docker compose -p bako-safe_dev --env-file .env.chain up -d --build + # Wait for healthy: curl http://127.0.0.1:4000/v1/health + ``` + +5. Start Socket Server: + ```bash + cd packages/socket-server && docker compose up -d --build + ``` + +6. Start API: + ```bash + cd packages/api && pnpm dev + ``` + +### Environment Variables + +Key environment variables in `packages/api/.env`: + +| Variable | Description | Default | +|----------|-------------|---------| +| `DATABASE_HOST` | PostgreSQL host | `127.0.0.1` | +| `DATABASE_PORT` | PostgreSQL port | `5432` | +| `REDIS_URL_WRITE` | Redis write URL | `redis://localhost:6379` | +| `REDIS_URL_READ` | Redis read URL | `redis://localhost:6379` | +| `FUEL_PROVIDER` | Fuel network GraphQL endpoint | `http://127.0.0.1:4000/v1/graphql` | +| `SOCKET_URL` | Socket server URL | `http://localhost:3001` | +| `UI_URL` | Frontend URL (for CORS) | `http://localhost:5174` | +| `RIG_ID_CONTRACT` | RIG contract address (optional in dev) | - | + +See `packages/api/.env.example` for the complete list with descriptions. + +## Database + +### Migrations + +Migrations are managed by TypeORM and run automatically on API startup. + +To run migrations manually: +```bash +cd packages/api && pnpm migration:run +``` + +To create a new migration: +```bash +cd packages/api && pnpm migration:create +``` + +To revert the last migration: +```bash +cd packages/api && pnpm migration:revert +``` + +### Utilities + +Populate database with test data: +```bash +cd packages/api && pnpm database:populate +``` + +Clear all database data: +```bash +cd packages/api && pnpm database:clear +``` + +## Tests + +Run tests with testcontainers (recommended, no manual setup needed): +```bash +cd packages/api && pnpm test:build +``` + +Or with the development environment running: +```bash +cd packages/api && pnpm test +``` + +## API Endpoints + +Base URL: `http://localhost:3333` + +| Route | Description | +|-------|-------------| +| `GET /ping` | Health check with timestamp | +| `GET /healthcheck` | Simple health check | +| `/auth/*` | Authentication endpoints | +| `/user/*` | User management | +| `/workspace/*` | Workspace management | +| `/predicate/*` | Predicate (vault) operations | +| `/transaction/*` | Transaction management | +| `/notifications/*` | User notifications | +| `/address-book/*` | Address book management | +| `/api-token/*` | API token management | +| `/cli/*` | CLI authentication | +| `/connections/*` | dApp connections | + +## Troubleshooting + +### Docker Compose Version Error + +If you see "client version is too old", ensure you're using Docker Compose V2: +```bash +docker compose version # Should show v2.x.x +``` + +### Fuel Provider Connection Error + +Ensure the fuel-core container is running and healthy: +```bash +docker ps | grep fuel-core +curl http://127.0.0.1:4000/v1/health +``` + +### Port Already in Use + +Stop any running containers and processes: +```bash +docker ps -aq | xargs docker stop +pkill -f "ts-node-dev" +``` + +### Database Connection Error + +Verify PostgreSQL is running and accessible: +```bash +docker ps | grep postgres +docker logs postgres +``` + +### Network Not Found + +Create the Docker network: +```bash +docker network create bako-network +``` + +### Socket Server SSL Error + +If socket-server fails with "server does not support SSL connections", ensure `DATABASE_HOST` is set to a local value (`127.0.0.1`, `localhost`, `db`, or `postgres`). + +## Cleanup + +Stop all containers: +```bash +docker ps -aq | xargs docker stop && docker ps -aq | xargs docker rm +``` + +Remove volumes (warning: deletes data): +```bash +docker volume ls -q | grep -E "bako|fuel" | xargs docker volume rm +``` + +## License + +Apache-2.0 diff --git a/TEST_ANALYSIS.md b/TEST_ANALYSIS.md new file mode 100644 index 000000000..f66804961 --- /dev/null +++ b/TEST_ANALYSIS.md @@ -0,0 +1,310 @@ +# Análise de Estabilidade de Testes - bako-safe-api + +> **Data:** 2026-02-05 +> **Branch:** `staging-docs-review` +> **Autor:** Revisão de onboarding + +--- + +## Resumo Executivo + +| Métrica | Valor | Status | +|---------|-------|--------| +| Total de Testes | 73 | ✅ | +| Testes Passando | 73 | ✅ | +| Testes Falhando | 0 | ✅ | +| Cobertura de Módulos | 8/8 (100%) | ✅ | +| Testes Unitários | 0 | ⚠️ | +| CI Configurado | Sim (PRs + push main/staging) | ✅ | + +--- + +## Setup de Testes + +### Stack Utilizada + +- **Test Runner:** Node.js native test runner (`node:test`) +- **HTTP Testing:** supertest +- **Database:** Testcontainers (PostgreSQL isolado) +- **Blockchain:** `launchTestNode()` do Fuel SDK +- **Assertions:** `node:assert/strict` + +### Como Rodar + +```bash +cd packages/api +pnpm test:build # Build + testes com testcontainers +``` + +### Validação do Setup + +| Item | Status | Observação | +|------|--------|------------| +| Testcontainers PostgreSQL | ✅ OK | Sobe container automaticamente | +| Fuel Test Node | ⚠️ Parcial | Incompatibilidade de versão | +| Build antes de testes | ✅ OK | Compila TS para JS | +| Cleanup após testes | ✅ OK | `t.after()` + `App.stop()` | +| CI GitHub Actions | ✅ OK | Roda em PRs | + +--- + +## Problema Identificado no Setup + +### Incompatibilidade de Versão fuel-core vs SDK + +**Erro observado:** +``` +InsufficientFeeAmount { expected: 1430, provided: 1000 } + +The Fuel Node that you are trying to connect to is using fuel-core version 0.47.1. +The TS SDK currently supports fuel-core version 0.43.1. +Things may not work as expected. +``` + +**Causa:** O `launchTestNode()` do SDK sobe um fuel-core 0.47.1, mas o SDK `fuels@0.101.3` espera 0.43.1. + +**Impacto:** Teste `transaction.tests.ts` falha ao criar mock de transação (fee calculation incorreto). + +**Solução sugerida:** +1. Atualizar `fuels` para versão compatível com fuel-core 0.47.1 +2. OU fixar versão do fuel-core no testcontainers + +--- + +## Cobertura por Módulo + +### Módulos COM Testes + +| Módulo | Arquivo | Endpoints | Testes | Cobertura | +|--------|---------|-----------|--------|-----------| +| auth | `auth.tests.ts` | 4 | 4 | 100% | +| user | `user.tests.ts` | 5 | 4 | 80% | +| predicate | `predicate.tests.ts` | 10 | 9 | 90% | +| transaction | `transaction.tests.ts` | 12 | 14 | 100%+ | +| addressBook | `addressBook.tests.ts` | 4 | 4 | 100% | +| apiToken | `apiToken.tests.ts` | 3 | 3 | 100% | +| notification | `notification.tests.ts` | 3 | 2 | 66% | + +### Módulos Anteriormente SEM Testes (CORRIGIDO ✅) + +| Módulo | Endpoints | Testes | Status | +|--------|-----------|--------|--------| +| workspace | 7 | 9 | ✅ CORRIGIDO | +| dApps/connections | 9 | 10 | ✅ CORRIGIDO | +| cliToken | 3 | 4 | ✅ CORRIGIDO | +| external | 4 | 0 | ⚠️ P2 | + +--- + +## Detalhamento dos Testes Existentes + +### auth.tests.ts (4 testes) +- ✅ `POST /user` - criar usuário e autenticar +- ✅ `POST /auth/code` - regenerar código de autenticação +- ✅ `POST /auth/code` - gerar código com sucesso +- ✅ `DELETE /auth/sign-out` - logout + +### user.tests.ts (4 testes) +- ✅ `PUT /user/:id` - atualizar nickname +- ✅ `GET /user/predicates` - listar predicates do usuário +- ✅ `GET /user/latest/transactions` - listar transações recentes +- ✅ `GET /user/latest/tokens` - obter valores USD dos tokens + +### predicate.tests.ts (9 testes) +- ✅ `POST /predicate` - criar com versão +- ✅ `POST /predicate` - criar sem versão +- ✅ `GET /predicate` - listar com paginação +- ✅ `GET /predicate/:id` - buscar por ID +- ✅ `GET /predicate/by-name/:name` - buscar por nome +- ✅ `GET /predicate/by-address/:address` - buscar por endereço +- ✅ `GET /predicate/reserved-coins/:id` - obter balance +- ✅ `GET /predicate/check/by-address/:address` - verificar existência +- ✅ `PUT /predicate/:address/visibility` - toggle visibilidade + +### transaction.tests.ts (14 testes) +- ✅ `POST /transaction` - criar transação +- ✅ `GET /transaction` - listar transações +- ✅ `GET /transaction?page&perPage` - listar com paginação +- ✅ `GET /transaction?status[]` - filtrar por status +- ✅ `GET /transaction/:id` - buscar por ID +- ✅ `GET /transaction/by-hash/:hash` - buscar por hash +- ✅ `GET /transaction/history/:id/:predicateId` - histórico +- ✅ `GET /transaction/pending` - transações pendentes +- ✅ `PUT /transaction/sign/:hash` - assinar transação +- ✅ `GET /transaction/:id/advanced-details` - detalhes avançados +- ✅ `GET /transaction/with-incomings` - transações com incomings +- ✅ `PUT /transaction/close/:id` - fechar transação +- ✅ `PUT /transaction/cancel/:hash` - cancelar transação +- ✅ Fluxo completo: criar → cancelar → recriar → assinar + +### addressBook.tests.ts (4 testes) +- ✅ `POST /address-book` - criar entrada +- ✅ `PUT /address-book/:id` - atualizar +- ✅ `GET /address-book` - listar +- ✅ `DELETE /address-book/:id` - deletar + +### apiToken.tests.ts (3 testes) +- ✅ `POST /api-token/:predicateId` - criar token +- ✅ `GET /api-token/:predicateId` - listar tokens +- ✅ `DELETE /api-token/:predicateId/:apiTokenId` - deletar + +### notification.tests.ts (2 testes) +- ✅ `GET /notifications` - listar com paginação e filtros +- ✅ `PUT /notifications/read-all` - marcar todas como lidas + +### cliToken.tests.ts (0 testes ativos) +- ❌ `Encode` - **COMENTADO** +- ❌ `Decode` - **COMENTADO** +- ❌ `Decode with invalid token` - **COMENTADO** + +--- + +## Endpoints SEM Cobertura de Testes + +### workspace (7 endpoints) - CRÍTICO + +```typescript +// packages/api/src/modules/workspace/routes.ts +router.get('/by-user', ...) // listar workspaces do usuário +router.post('/', ...) // criar workspace +router.get('/:id', ...) // buscar por ID +router.put('/', ...) // atualizar workspace +router.put('/permissions/:member', ...) // atualizar permissões +router.post('/members/:member/remove', ...) // remover membro +router.post('/members/:member/include', ...) // adicionar membro +``` + +### dApps/connections (9 endpoints) - CRÍTICO + +```typescript +// packages/api/src/modules/dApps/routes.ts +router.post('/', ...) // conectar dApp +router.get('/:sessionId/transaction/:vaultAddress/:txId', ...) // código conector +router.put('/:sessionId/network', ...) // mudar rede +router.get('/:sessionId/state', ...) // estado da sessão +router.get('/:sessionId/accounts', ...) // contas disponíveis +router.get('/:sessionId/currentAccount', ...) // conta atual +router.get('/:sessionId/currentNetwork', ...) // rede atual +router.get('/:sessionId', ...) // sessão atual +router.delete('/:sessionId', ...) // desconectar +``` + +### external (4 endpoints) + +```typescript +// packages/api/src/modules/external/routes.ts +router.get('/predicate', ...) // listar predicates (API externa) +router.get('/user', ...) // listar users (API externa) +router.get('/quote', ...) // cotações +router.get('/tx', ...) // transações +``` + +--- + +## CI/CD + +### Configuração Atual + +```yaml +# .github/workflows/test-api.yml +name: Run API Tests + +on: + pull_request: + branches: + - "**" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-forc + - run: npm install -g pnpm + - uses: actions/setup-node@v4 + - run: pnpm install --no-frozen-lockfile + - run: cp .env.test .env + - run: pnpm test:build +``` + +### Problemas Identificados + +1. **Apenas em PRs** - Não roda em push para `main`/`master` +2. **Sem coverage report** - Não há métricas de cobertura +3. **Sem badge de status** - README não mostra status dos testes + +--- + +## Plano de Ação + +### P0 - Crítico (Fazer Agora) + +- [ ] **Corrigir incompatibilidade fuel-core vs SDK** + - Atualizar `fuels` ou fixar versão do fuel-core + - Responsável: ___ + - Prazo: ___ + +- [ ] **Adicionar testes para workspace** + - CRUD de workspaces + - Permissões (owner, admin, manager, viewer) + - Adicionar/remover membros + +- [ ] **Descomentar ou remover cliToken.tests.ts** + - Testes comentados causam falsa sensação de cobertura + +### P1 - Alta Prioridade + +- [ ] **Adicionar testes para dApps/connections** + - Fluxo de conexão completo + - Mudança de rede + - Disconnect + +- [ ] **Configurar coverage report** + - Adicionar `c8` ou `nyc` + - Threshold mínimo: 70% + - Falhar CI se abaixo do threshold + +- [ ] **CI em push para branches principais** + - Adicionar trigger: `push: branches: [main, staging]` + +### P2 - Média Prioridade + +- [ ] **Adicionar testes para external routes** +- [ ] **Adicionar testes unitários para services** +- [ ] **Adicionar testes de edge cases** (validações, erros 4xx/5xx) +- [ ] **Badge de status no README** + +--- + +## Conclusão + +### Os testes validam que o sistema continua funcionando? + +**PARCIALMENTE** + +| Aspecto | Validado? | +|---------|-----------| +| Autenticação | ✅ Sim | +| Gestão de Vaults (predicates) | ✅ Sim | +| Transações | ✅ Sim | +| Address Book | ✅ Sim | +| API Tokens | ✅ Sim | +| Notificações | ✅ Sim | +| **Workspaces/Permissões** | ❌ **NÃO** | +| **Integrações dApps** | ❌ **NÃO** | +| **CLI** | ❌ **NÃO** | + +### Risco de Regressão + +- **ALTO** para workspace e dApps (sem cobertura) +- **MÉDIO** para notification e external (cobertura parcial) +- **BAIXO** para auth, predicate, transaction (boa cobertura) + +--- + +## Referências + +- Arquivos de teste: `packages/api/src/tests/*.tests.ts` +- Setup de teste: `packages/api/src/tests/utils/Setup.ts` +- CI: `.github/workflows/test-api.yml` +- Mocks: `packages/api/src/tests/mocks/` diff --git a/package.json b/package.json index 539a0ad2a..c18b79c09 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "./packages/*" ], "scripts": { + "predev": "docker network create bako-network 2>/dev/null || true", "start:prod": "turbo run start:prod", "start:stg": "turbo run start:stg", "dev": "turbo run dev", @@ -16,6 +17,27 @@ "start:environment": "chmod +x script.sh && ./script.sh" }, "devDependencies": { - "turbo": "^1.13.3" + "turbo": "^1.13.4" + }, + "pnpm": { + "overrides": { + "glob@>=10.2.0 <10.5.0": "10.5.0", + "braces@<3.0.3": "3.0.3", + "semver@>=7.0.0 <7.5.2": "7.5.2", + "qs@<6.14.1": "6.14.1", + "trim-newlines@<3.0.1": "3.0.1", + "js-yaml@<3.13.1": "3.14.1", + "uglify-js@<2.6.0": "3.19.3", + "minimatch@<3.1.4": "3.1.4", + "minimatch@>=5.0.0 <5.1.8": "5.1.8", + "minimatch@>=9.0.0 <9.0.7": "9.0.7", + "minimatch@>=10.0.0 <10.2.3": "10.2.3" + }, + "auditConfig": { + "ignoreCves": [ + "CVE-2017-16115", + "CVE-2026-0775" + ] + } } } diff --git a/packages/api/.env.example b/packages/api/.env.example index 3e02afae2..1c7db8f4c 100644 --- a/packages/api/.env.example +++ b/packages/api/.env.example @@ -5,7 +5,7 @@ DATABASE_USERNAME=postgres DATABASE_PASSWORD=postgres DATABASE_NAME=postgres -# Database Metabase User +# Database Metabase User (optional for local dev) DB_METABASE_USERNAME= DB_METABASE_PASS= @@ -17,49 +17,83 @@ API_ENVIRONMENT=development API_TOKEN_SECRET=api_token_secret API_TOKEN_SECRET_IV=api_token_secret_iv API_SOCKET_SESSION_ID=me389p6z493z +NODE_ENV=development -UI_URL=http://localhost:5175 +# URLs +UI_URL=http://localhost:5174 API_URL=http://localhost:3333 SOCKET_URL=http://localhost:3001 +WORKER_URL=http://localhost:3063 + +# Fuel Provider +# For local dev with fuel-core container: FUEL_PROVIDER=http://127.0.0.1:4000/v1/graphql -# FUEL_PROVIDER_CHAIN_ID=9889 +# For testnet: +# FUEL_PROVIDER=https://testnet.fuel.network/v1/graphql FUEL_PROVIDER_CHAIN_ID=0 +# Assets ASSETS_URL=https://besafe-asset.s3.amazonaws.com/icon -ASSETS_URL=https://besafe-asset.s3.amazonaws.com/icon - -#assets -COIN_MARKET_CAP_API_KEY= GAS_LIMIT=10000000 MAX_FEE=1000000 -# Admin user -APP_ADMIN_EMAIL=admin_user_email -APP_ADMIN_PASSWORD=admin_user_password - +# Redis (required) +REDIS_URL_WRITE=redis://localhost:6379 +REDIS_URL_READ=redis://localhost:6379 -# ADMIN USER +# Admin user APP_ADMIN_EMAIL=admin_user_email APP_ADMIN_PASSWORD=admin_user_password -# TOKENS +# Tokens ACCESS_TOKEN_SECRET=access_token_secret REFRESH_TOKEN_SECRET=refresh_token_secret -# AWS - +# AWS (optional for local dev) AWS_SMTP_USER= AWS_SMTP_PASS= -# EMAIL +# Email EMAIL_FROM="Bako Safe " -MAIL_TESTING_NOTIFICATIONS=guilhermemigroque@gmail.com +MAIL_TESTING_NOTIFICATIONS= -# COIN MARKET CAP API +# External APIs (optional for local dev) COIN_MARKET_CAP_API_KEY= -# MONITORING +# Monitoring (optional) SENTRY_DNS= -# RIG -RIG_ID_CONTRACT= \ No newline at end of file +# RIG Contract (mainnet address - required for price feed features) +# Leave empty to disable RIG features in development +RIG_ID_CONTRACT= + +# LayerSwap Integration (optional) +LAYERS_SWAP_API_URL=https://api.layerswap.io/api/v2 +LAYERS_SWAP_API_KEY_SANDBOX= +LAYERS_SWAP_API_KEY_PROD= +LAYERS_SWAP_WEBHOOK_SECRET= + +# MELD Integration (optional) +MELD_SANDBOX_API_KEY= +MELD_SANDBOX_API_URL=https://api-sb.meld.io/ +MELD_SANDBOX_WEBHOOK_SECRET= +MELD_PRODUCTION_API_KEY= +MELD_PRODUCTION_API_URL=https://api.meld.io/ +MELD_PRODUCTION_WEBHOOK_SECRET= + +# Cache Configuration +ENABLE_BALANCE_CACHE=true +BALANCE_CACHE_TTL=300 +BALANCE_INVALIDATION_TTL=3600 +WARMUP_ENABLED=true +WARMUP_CONCURRENCY=5 +WARMUP_MAX_PREDICATES=20 +WARMUP_SKIP_CACHED=true +TRANSACTION_CACHE_TTL=600 +TRANSACTION_INCREMENTAL_LIMIT=10 + +# Internal API (for Worker integration) +INTERNAL_API_KEY=worker_api_key + +# Socket Client +SOCKET_CLIENT_DISCONNECT_TIMEOUT=30000 diff --git a/packages/api/.env.test b/packages/api/.env.test index 40d509334..14ae0fd4c 100644 --- a/packages/api/.env.test +++ b/packages/api/.env.test @@ -6,8 +6,8 @@ DATABASE_PASSWORD=postgres DATABASE_NAME=postgres # App -API_PORT=3333 -API_NAME=bsafe-api +API_PORT=3334 +API_NAME=bsafe-apix API_DOCKERFILE= API_ENVIRONMENT=development API_SOCKET_SESSION_ID=me389p6z493z @@ -15,7 +15,7 @@ API_SOCKET_SESSION_ID=me389p6z493z UI_URL=http://localhost:5173 API_URL=http://localhost:3333 SOCKET_URL=http://localhost:3001 -FUEL_PROVIDER='https://mainnet.fuel.network/v1/graphql' +FUEL_PROVIDER='https://testnet.fuel.network/v1/graphql' # FUEL_PROVIDER='http://127.0.0.1:4000/v1/graphql' # FUEL_PROVIDER=https://bako:LR2RU3jQHPlbqog3tnDmZw@mainnet.fuel.network/v1/graphql FUEL_PROVIDER_CHAIN_ID=0 @@ -53,4 +53,15 @@ AWS_SMTP_PASS="AWS_SMTP_PASS" MAIL_TESTING_NOTIFICATIONS=guilhermemigroque@gmail.com # REDIS -REDIS_URL= \ No newline at end of file +REDIS_URL= + +MELD_SANDBOX_API_KEY=asd +MELD_SANDBOX_API_URL=https://sandbox.com/api +MELD_SANDBOX_WEBHOOK_SECRET=test_secret +MELD_PRODUCTION_API_KEY=asd +MELD_PRODUCTION_API_URL=https://sandbox.com/api +MELD_PRODUCTION_WEBHOOK_SECRET=test_secret + +RIG_ID_CONTRACT=0x2181f1b8e00756672515807cab7de10c70a9b472a4a9b1b6ca921435b0a1f49b + +NODE_ENV=test \ No newline at end of file diff --git a/packages/api/.eslintrc.js b/packages/api/.eslintrc.js index 6daefb074..2361e061e 100644 --- a/packages/api/.eslintrc.js +++ b/packages/api/.eslintrc.js @@ -20,6 +20,23 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': ['off'], // Allow inferred function return type '@typescript-eslint/no-unused-vars': ['off'], // Enable TS no unused var role '@typescript-eslint/no-explicit-any': ['warn'], // Block "any" as a type + 'no-console': 'error', // Disallow all console.* methods - use logger instead }, - ignorePatterns: ['node_modules'], + ignorePatterns: ['node_modules', 'src/contracts/**/*', 'build'], + overrides: [ + { + // Allow console only in scripts and migrations (non-production code) + files: ['src/scripts/**/*.ts', 'src/database/migrations/**/*.ts'], + rules: { + 'no-console': 'off', + }, + }, + { + // Allow @ts-nocheck in config files (TypeORM dynamic config) + files: ['src/config/**/*.ts'], + rules: { + '@typescript-eslint/ban-ts-comment': 'off', + }, + }, + ], }; diff --git a/packages/api/CACHE-BALANCE-IMPLEMENTATION.md b/packages/api/CACHE-BALANCE-IMPLEMENTATION.md new file mode 100644 index 000000000..67629755e --- /dev/null +++ b/packages/api/CACHE-BALANCE-IMPLEMENTATION.md @@ -0,0 +1,1242 @@ +# Cache de Balances - Implementação Completa + +## Visão Geral + +### Problema Atual + +A API realiza chamadas síncronas à blockchain da Fuel durante requisições HTTP para buscar balances de predicates, causando: + +- **Latência alta**: ~500ms por predicate +- **Dependência externa**: Se o provider Fuel estiver lento/offline, a API fica lenta/indisponível +- **Escalabilidade limitada**: Múltiplas requisições simultâneas sobrecarregam o provider +- **Custo operacional**: Chamadas desnecessárias para dados que mudam raramente + +### Endpoints Afetados + +1. `GET /api/predicate/:id/allocation` - Alocação de assets (usado em dashboards) +2. `GET /api/predicate/:id/reserved-coins` - Balances detalhados (usado em cards de predicate) +3. `GET /api/user/allocation` - Alocação total do usuário + +### Solução Proposta + +Implementar cache em Redis no nível do **Provider**, de forma **transparente** para os services existentes: + +- Cache de balances com TTL de 10 minutos +- Invalidação inteligente quando transações são confirmadas +- Warm-up automático no login do usuário +- Diferenciação por rede (chainId) + +--- + +## Arquitetura + +### Fluxo de Cache + +``` +┌─────────────────────────────────────────────────────────┐ +│ Service/Controller (Código Existente) │ +│ const instance = await instancePredicate(...) │ +│ const result = await instance.getBalances() │ +│ (Nenhuma mudança necessária!) │ +└────────────────────────┬────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Vault (bakosafe) │ +│ vault.getBalances() { │ +│ return this.provider.getBalances(this.address) │ +│ } │ +└────────────────────────┬────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ ProviderWithCache (Nosso Wrapper) │ +│ async getBalances(address) { │ +│ 1. const cached = await balanceCache.get(addr, id) │ +│ 2. if (cached) return { balances: cached } │ +│ 3. const result = await super.getBalances(address) │ +│ 4. await balanceCache.set(addr, result, id) │ +│ 5. return result │ +│ } │ +└────────────────────────┬────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ BalanceCache (Redis Manager) │ +│ - Serializa/Deserializa BigNumbers │ +│ - Gerencia keys: balance:{addr}:{chainId} │ +│ - Verifica flags de invalidação │ +└────────────────────────┬────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Redis Storage │ +│ Key: balance:0xfuel123...:9889 │ +│ Value: {"balances": [...], "timestamp": 1234...} │ +│ TTL: 600 segundos (10 minutos) │ +└─────────────────────────────────────────────────────────┘ +``` + +### Fluxo de Invalidação + +``` +┌─────────────────────────────────────────────────────────┐ +│ Transação Confirmada na Blockchain │ +│ TransactionService.sendToChain() → SUCCESS │ +└────────────────────────┬────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Invalidar Cache Imediatamente │ +│ balanceCache.invalidate(predicateAddress) │ +│ - Remove: balance:{address}:{chainId} │ +│ - Seta flag: balance:invalidated:{address} │ +└────────────────────────┬────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Próxima Requisição ao Endpoint │ +│ ProviderWithCache.getBalances() │ +│ - Detecta flag de invalidação │ +│ - Busca dados frescos da blockchain │ +│ - Atualiza cache │ +└─────────────────────────────────────────────────────────┘ +``` + +### Fluxo de Warm-up + +``` +┌─────────────────────────────────────────────────────────┐ +│ User faz Login │ +│ POST /api/auth/sign-in │ +└────────────────────────┬────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ AuthController.signIn() │ +│ 1. Valida credenciais │ +│ 2. Cria sessão │ +│ 3. return successful(signin) ← Response imediato │ +│ 4. warmupUserBalances() (background, não aguarda) │ +└────────────────────────┬────────────────────────────────┘ + ↓ (paralelo) +┌─────────────────────────────────────────────────────────┐ +│ Warm-up em Background (~2-3s) │ +│ 1. Busca todos os predicates do user │ +│ 2. Para cada predicate: │ +│ provider.getBalances(predicateAddress) │ +│ 3. ProviderWithCache cacheia automaticamente │ +└────────────────────────┬────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ User acessa Dashboard │ +│ GET /api/predicate/:id/allocation │ +│ - Cache HIT! (~50ms) │ +│ GET /api/predicate/:id/reserved-coins │ +│ - Cache HIT! (~30ms) │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## Estrutura de Chaves Redis + +### Cache de Balances + +``` +Padrão: balance:{predicateAddress}:{chainId} + +Exemplos: +balance:0xfuel139e53d4a8b4a1ed2088d5c67ef04fe3e09c0eefa5ce23b36c5e4b59e0933b9:9889 +balance:0xfuel139e53d4a8b4a1ed2088d5c67ef04fe3e09c0eefa5ce23b36c5e4b59e0933b9:0 + +TTL: 600 segundos (10 minutos) + +Formato do valor: +{ + "balances": [ + { + "assetId": "0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07", + "amount": "0x3b9aca00" // hex string do BigNumber + } + ], + "timestamp": 1764104869733, + "chainId": 9889, + "networkUrl": "https://testnet.fuel.network/v1/graphql" +} +``` + +### Flags de Invalidação + +``` +Padrão: balance:invalidated:{predicateAddress} + +Exemplos: +balance:invalidated:0xfuel139e53d4a8b4a1ed2088d5c67ef04fe3e09c0eefa5ce23b36c5e4b59e0933b9 + +TTL: 3600 segundos (1 hora) + +Formato do valor: +"1764104869733" // timestamp da invalidação +``` + +**Nota**: Flag de invalidação não tem chainId - invalida TODAS as redes do mesmo predicate. + +--- + +## Componentes + +### 1. ProviderWithCache + +**Arquivo**: `packages/api/src/utils/ProviderWithCache.ts` + +Wrapper transparente do `Provider` do Fuel SDK que intercepta chamadas a `getBalances()` e adiciona cache. + +**Características**: +- Estende `Provider` nativo do Fuel SDK +- Override do método `getBalances(address: string)` +- Mantém mesma assinatura e output +- Lazy load do chainId (busca uma vez e mantém) +- Fallback automático em caso de erro no cache + +**Interface**: +```typescript +class ProviderWithCache extends Provider { + async getBalances(address: string): Promise<{ balances: CoinQuantity[] }> + async getBalancesForceRefresh(address: string): Promise<{ balances: CoinQuantity[] }> +} +``` + +### 2. BalanceCache + +**Arquivo**: `packages/api/src/server/storage/balance.ts` + +Gerenciador de cache no Redis com suporte a BigNumbers. + +**Características**: +- Singleton pattern +- Serialização de BigNumbers para hex strings +- Deserialização de hex strings para BigNumbers +- Verificação de flags de invalidação +- Diferenciação por chainId + +**Interface**: +```typescript +class BalanceCache { + async get(predicateAddress: string, chainId: number): Promise + async set(predicateAddress: string, balances: CoinQuantity[], chainId: number, networkUrl: string): Promise + async invalidate(predicateAddress: string): Promise + async clearInvalidation(predicateAddress: string): Promise + async clear(predicateAddress: string, chainId?: number): Promise + async stats(): Promise<{ type: string; ttl: number }> +} +``` + +### 3. FuelProvider (Modificado) + +**Arquivo**: `packages/api/src/utils/FuelProvider.ts` + +**Mudanças**: +- Retorna `ProviderWithCache` em vez de `Provider` normal +- Desabilita cache nativo do Fuel SDK (`resourceCacheTTL: 0`) +- Mantém pool de providers em memória (não muda) + +### 4. RedisWriteClient (Atualizado) + +**Arquivo**: `packages/api/src/utils/redis/RedisWriteClient.ts` + +**Mudanças**: +- Adicionar método `del(key: string)` para deletar chaves +- Atualizar método `set()` para aceitar options com TTL customizado + +--- + +## Pontos de Invalidação + +### 1. TransactionService.sendToChain() + +**Arquivo**: [`packages/api/src/modules/transaction/services.ts`](packages/api/src/modules/transaction/services.ts) +**Linha**: ~637 + +**Quando**: Após transação ser enviada e confirmada com sucesso na blockchain + +**Implementação**: +```typescript +async sendToChain(hash: string, network: Network) { + // ... código existente ... + + try { + const transactionResponse = await vault.send(tx); + const { gasUsed } = await transactionResponse.waitForResult(); + + const _api_transaction: IUpdateTransactionPayload = { + status: TransactionStatus.SUCCESS, + sendTime: new Date(), + gasUsed: gasUsed.format(), + resume: { + ...resume, + gasUsed: gasUsed.format(), + status: TransactionStatus.SUCCESS, + }, + }; + + await new NotificationService().transactionSuccess(id, network); + + // Invalidar cache após sucesso + await this.invalidatePredicateBalance(transaction.predicateAddress); + + return await this.update(id, _api_transaction); + } catch (e) { + // ... erro ... + } +} + +// Método auxiliar +private async invalidatePredicateBalance(predicateAddress: string): Promise { + try { + const balanceCache = App.getInstance()._balanceCache; + await balanceCache.invalidate(predicateAddress); + console.log(`[TX_CONFIRMED] Balance invalidated for ${predicateAddress}`); + } catch (error) { + console.error('[TX_CONFIRMED] Failed to invalidate cache:', error); + } +} +``` + +### 2. TransactionController.close() + +**Arquivo**: [`packages/api/src/modules/transaction/controller.ts`](packages/api/src/modules/transaction/controller.ts) +**Linha**: ~747 + +**Quando**: Transação é fechada manualmente (via endpoint de close) + +**Implementação**: +```typescript +async close({ + body: { gasUsed, transactionResult }, + params: { id }, +}: ICloseTransactionRequest) { + try { + // Buscar predicate address antes de atualizar + const transaction = await Transaction.findOne({ + where: { id }, + relations: ['predicate'], + }); + + const response = await this.transactionService.update(id, { + status: TransactionStatus.SUCCESS, + sendTime: new Date(), + gasUsed, + resume: transactionResult, + }); + + // Invalidar cache após fechar transação + if (transaction?.predicate?.predicateAddress) { + await this.invalidatePredicateBalance( + transaction.predicate.predicateAddress + ); + } + + return successful(response, Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } +} + +// Método auxiliar +private async invalidatePredicateBalance(predicateAddress: string): Promise { + try { + const balanceCache = App.getInstance()._balanceCache; + await balanceCache.invalidate(predicateAddress); + console.log(`[TX_CLOSED] Balance invalidated for ${predicateAddress}`); + } catch (error) { + console.error('[TX_CLOSED] Failed to invalidate cache:', error); + } +} +``` + +### 3. Endpoint Interno (Worker Integration) + +**Arquivo**: [`packages/api/src/modules/webhook/routes.ts`](packages/api/src/modules/webhook/routes.ts) + +**Quando**: Worker detecta mudança de balance no MongoDB e notifica a API + +**Implementação**: +```typescript +router.post('/internal/invalidate-balance', async (req, res) => { + try { + const { predicate_address } = req.body; + + if (!predicate_address) { + return res.status(400).json({ error: 'predicate_address required' }); + } + + const balanceCache = App.getInstance()._balanceCache; + await balanceCache.invalidate(predicate_address); + + return res.json({ + success: true, + message: `Balance invalidated for ${predicate_address}`, + timestamp: Date.now(), + }); + } catch (error) { + console.error('[INVALIDATE_BALANCE_ERROR]', error); + return res.status(500).json({ error: error.message }); + } +}); + +// Endpoint de debug +router.get('/internal/cache-stats', async (req, res) => { + try { + const balanceCache = App.getInstance()._balanceCache; + const stats = await balanceCache.stats(); + return res.json(stats); + } catch (error) { + return res.status(500).json({ error: error.message }); + } +}); +``` + +--- + +## Estratégia de Warm-up + +### Implementação no Login + +**Arquivo**: [`packages/api/src/modules/auth/controller.ts`](packages/api/src/modules/auth/controller.ts) +**Linha**: ~44 + +### Quando Executar + +- **Após login bem-sucedido** (signIn) +- **Em background** (não bloqueia response) +- **Para todos os predicates** do usuário + +### Código + +```typescript +export class AuthController { + // ... código existente ... + + /** + * Pré-aquece cache de balances para todos os predicates do usuário + * Executa em background para não bloquear o login + */ + private async warmupUserBalances( + userId: string, + network: Network + ): Promise { + try { + console.time(`[WARMUP] User ${userId}`); + + // 1. Buscar workspaces do usuário + const workspaces = await new WorkspaceService() + .filter({ user: userId }) + .list(); + + const workspaceIds = Array.isArray(workspaces) + ? workspaces.map(w => w.id) + : workspaces.data.map(w => w.id); + + // 2. Buscar predicates desses workspaces + const predicates = await new PredicateService() + .filter({ workspace: workspaceIds }) + .list(); + + const predicateList = Array.isArray(predicates) + ? predicates + : predicates.data; + + if (predicateList.length === 0) { + console.log(`[WARMUP] No predicates found for user ${userId}`); + return; + } + + console.log( + `[WARMUP] Found ${predicateList.length} predicates, fetching balances...` + ); + + // 3. Buscar balances em paralelo + const provider = await FuelProvider.create(network.url); + + const results = await Promise.allSettled( + predicateList.map(async predicate => { + try { + // ProviderWithCache cacheia automaticamente + await provider.getBalances(predicate.predicateAddress); + return { success: true, address: predicate.predicateAddress }; + } catch (err) { + return { + success: false, + address: predicate.predicateAddress, + error: err.message + }; + } + }) + ); + + const successful = results.filter(r => + r.status === 'fulfilled' && r.value.success + ).length; + + console.timeEnd(`[WARMUP] User ${userId}`); + console.log(`[WARMUP] Success: ${successful}/${predicateList.length}`); + } catch (error) { + console.error('[WARMUP] Error:', error); + } + } + + async signIn(req: ISignInRequest) { + try { + const { digest, encoder, signature, userAddress, name } = req.body; + const userFilter = userAddress + ? { address: new Address(userAddress).toB256() } + : { name }; + + const { userToken, signin } = await TokenUtils.createAuthToken( + signature, + digest, + encoder, + userFilter, + ); + + await App.getInstance()._sessionCache.addSession( + userToken.accessToken, + userToken, + ); + + // Disparar warm-up em background (não aguarda) + this.warmupUserBalances(signin.user_id, signin.network).catch(err => { + console.error('[WARMUP] Failed:', err); + }); + + return successful(signin, Responses.Ok); + } catch (e) { + if (e instanceof GeneralError) throw e; + return error(e.error, e.statusCode); + } + } +} +``` + +### Benefícios do Warm-up + +- **UX melhorada**: Dashboard carrega instantaneamente +- **Não bloqueia login**: Response imediato ao usuário +- **Paralelo**: Busca múltiplos balances simultaneamente +- **Resiliente**: Falhas no warm-up não afetam o login +- **Cache pronto**: Quando usuário acessar dashboard, cache já está aquecido + +--- + +## Checklist de Implementação + +### Fase 1: Infraestrutura Base + +- [ ] **1.1. Atualizar RedisWriteClient** + - Arquivo: `packages/api/src/utils/redis/RedisWriteClient.ts` + - Adicionar método `del(key: string)` + - Atualizar método `set()` para aceitar `options?: { EX?: number }` + +- [ ] **1.2. Criar BalanceCache** + - Arquivo: `packages/api/src/server/storage/balance.ts` + - Implementar serialização/deserialização de BigNumbers + - Métodos: get, set, invalidate, clearInvalidation, clear, stats + +- [ ] **1.3. Criar ProviderWithCache** + - Arquivo: `packages/api/src/utils/ProviderWithCache.ts` + - Estender `Provider` do Fuel SDK + - Override de `getBalances()` com cache + - Método adicional `getBalancesForceRefresh()` + +- [ ] **1.4. Modificar FuelProvider** + - Arquivo: `packages/api/src/utils/FuelProvider.ts` + - Mudar tipo de `providers` para `ProviderWithCache` + - Adicionar `PROVIDER_OPTIONS` com `resourceCacheTTL: 0` + - Usar `ProviderWithCache` no método `create()` + +- [ ] **1.5. Adicionar BalanceCache ao App** + - Arquivo: `packages/api/src/server/app.ts` + - Importar `BalanceCache` + - Adicionar propriedade `private balanceCache: BalanceCache` + - Inicializar no construtor + - Criar getter `_balanceCache` + +### Fase 2: Invalidação + +- [ ] **2.1. TransactionService.sendToChain()** + - Arquivo: `packages/api/src/modules/transaction/services.ts` + - Adicionar método privado `invalidatePredicateBalance()` + - Chamar após `TransactionStatus.SUCCESS` (linha ~647) + +- [ ] **2.2. TransactionController.close()** + - Arquivo: `packages/api/src/modules/transaction/controller.ts` + - Adicionar método privado `invalidatePredicateBalance()` + - Buscar `transaction.predicate.predicateAddress` antes de invalidar + - Chamar após update com SUCCESS (linha ~752) + +- [ ] **2.3. Endpoint de Invalidação** + - Arquivo: `packages/api/src/modules/webhook/routes.ts` + - Adicionar `POST /internal/invalidate-balance` + - Adicionar `GET /internal/cache-stats` (debug) + +### Fase 3: Warm-up + +- [ ] **3.1. AuthController.signIn()** + - Arquivo: `packages/api/src/modules/auth/controller.ts` + - Adicionar método privado `warmupUserBalances()` + - Importar `WorkspaceService` e `PredicateService` + - Disparar warm-up após login (sem await) + +### Fase 4: Validação + +- [ ] **4.1. Testes Manuais** + - Verificar cache hit/miss em logs + - Testar invalidação após transação + - Testar warm-up após login + - Verificar keys no Redis + +- [ ] **4.2. Monitoramento** + - Adicionar métricas de cache hit rate + - Logs estruturados para debug + - Alertas para falhas de cache + +--- + +## Exemplos de Código + +### Serialização de BigNumbers + +```typescript +// Serializar para salvar no Redis +private serializeBalances(balances: CoinQuantity[]): SerializedCoinQuantity[] { + return balances.map(b => ({ + assetId: b.assetId, + amount: b.amount.toHex(), // BN → hex string + })); +} + +// Deserializar ao buscar do Redis +private deserializeBalances(serialized: SerializedCoinQuantity[]): CoinQuantity[] { + return serialized.map(s => ({ + assetId: s.assetId, + amount: bn(s.amount), // hex string → BN + })); +} +``` + +### Uso Transparente no Código Existente + +**ANTES** (sem mudanças): +```typescript +// PredicateService.allocation() +const instance = await this.instancePredicate( + configurable, + network.url, + version, +); + +const balances = (await instance.getBalances()).balances.filter(a => + a.amount.gt(0), +); +``` + +**DEPOIS** (mesmo código, cache automático): +```typescript +// PredicateService.allocation() +const instance = await this.instancePredicate( + configurable, + network.url, + version, +); + +// Automaticamente usa cache do ProviderWithCache! +const balances = (await instance.getBalances()).balances.filter(a => + a.amount.gt(0), +); +``` + +### Verificar Cache no Redis + +```bash +# Conectar ao Redis +redis-cli + +# Listar todas as chaves de balance +KEYS balance:* + +# Ver conteúdo de um cache específico +GET "balance:0xfuel139e53...:9889" + +# Ver flag de invalidação +GET "balance:invalidated:0xfuel139e53..." + +# Ver TTL restante +TTL "balance:0xfuel139e53...:9889" + +# Deletar cache manualmente (debug) +DEL "balance:0xfuel139e53...:9889" +``` + +--- + +## Métricas de Performance + +### Antes da Implementação + +**Cenário**: Usuário com 10 predicates acessando dashboard + +``` +Timeline de Loading: +T+0ms: Login completo +T+50ms: GET /api/predicate (lista predicates) → 50ms +T+100ms: GET /api/predicate/1/reserved-coins → 500ms (blockchain) +T+600ms: GET /api/predicate/2/reserved-coins → 500ms (blockchain) +... +T+5s: GET /api/predicate/10/reserved-coins → 500ms (blockchain) +T+5.5s: GET /api/user/allocation → 2000ms (blockchain) +─────────────────────────────────────────────────── +TOTAL: ~7.5 segundos para carregar dashboard completo +``` + +**Chamadas à blockchain**: 11 (10 reserved-coins + 1 allocation) + +### Depois da Implementação (Com Cache) + +**Cenário**: Mesmo usuário, mesma situação + +``` +Timeline de Loading: +T+0ms: Login completo + Warm-up inicia em background +T+50ms: GET /api/predicate (lista predicates) → 50ms +T+100ms: GET /api/predicate/1/reserved-coins → 30ms (cache HIT) +T+130ms: GET /api/predicate/2/reserved-coins → 30ms (cache HIT) +... +T+370ms: GET /api/predicate/10/reserved-coins → 30ms (cache HIT) +T+400ms: GET /api/user/allocation → 200ms (cache HIT) +─────────────────────────────────────────────────── +TOTAL: ~600ms para carregar dashboard completo +``` + +**Chamadas à blockchain**: 0 (todos cache hits após warm-up) + +### Comparação + +| Métrica | Antes | Depois | Melhoria | +|---------|-------|--------|----------| +| **Tempo total de loading** | 7.5s | 600ms | **92% redução** | +| **Chamadas blockchain** | 11 | 0-11 | **0-100% redução** | +| **Latência por endpoint** | 500-2000ms | 30-200ms | **85-94% redução** | +| **Disponibilidade** | Depende do Fuel provider | Independente | **Muito maior** | +| **Custo operacional** | Alto (muitas RPC calls) | Baixo | **Redução significativa** | + +### Cache Hit Rate Esperado + +Baseado em padrões de uso típicos: + +- **Primeira hora após login**: 95-98% hit rate +- **Após transação confirmada**: 0% (invalidado, recalcula na próxima) +- **Após recalcular**: 95-98% hit rate novamente +- **Após 10 minutos sem uso**: 0% (TTL expirado) + +### Cenários de Performance + +#### Cenário 1: Login Fresh (Cache Vazio) +``` +Login → Warm-up (2s background) → Dashboard load +T+0s: Login response +T+2s: Warm-up completo (background) +T+2s: User acessa dashboard → 200ms (cache) +``` + +#### Cenário 2: Usuário Ativo (Cache Aquecido) +``` +Dashboard refresh a cada 30s: +- Request 1: 200ms (cache hit) +- Request 2: 200ms (cache hit) +- Request 3: 200ms (cache hit) +``` + +#### Cenário 3: Após Transação +``` +T+0s: Transação confirmada +T+0s: Cache invalidado +T+10s: User refresha dashboard → 2s (cache miss, busca blockchain) +T+40s: User refresha novamente → 200ms (cache hit) +``` + +--- + +## Troubleshooting + +### 1. Verificar se Cache está Funcionando + +#### Checar Logs +```bash +# Procurar por logs de cache +tail -f api.log | grep "BalanceCache\|ProviderCache\|WARMUP" + +# Logs esperados: +[BalanceCache] HIT 0xfuel123... chain:9889 (5.2s old) +[BalanceCache] SET 0xfuel123... chain:9889 +[WARMUP] Starting for user uuid-123 +[WARMUP] Found 10 predicates, fetching balances... +[WARMUP] Complete for user uuid-123 +``` + +#### Verificar Redis + +```bash +# Ver todas as chaves de balance +redis-cli KEYS "balance:*" | wc -l + +# Ver uma chave específica +redis-cli GET "balance:0xfuel139e53...:9889" + +# Ver flags de invalidação +redis-cli KEYS "balance:invalidated:*" + +# Monitorar operações em tempo real +redis-cli MONITOR | grep balance +``` + +### 2. Cache Não Está Sendo Usado (MISS constante) + +**Possíveis causas**: + +1. **TTL muito curto**: Verificar se `CACHE_TTL` está configurado corretamente (600s) +2. **ChainId diferente**: Verificar se chainId é consistente entre chamadas +3. **Endereço em formato diferente**: Verificar normalização de endereços +4. **Redis com problemas**: Verificar conexão com Redis + +**Debug**: +```typescript +// Adicionar logs temporários em ProviderWithCache +console.log('[DEBUG] Looking for cache key:', cacheKey); +console.log('[DEBUG] ChainId:', this.chainId); +console.log('[DEBUG] Cache result:', cached ? 'HIT' : 'MISS'); +``` + +### 3. Invalidação Não Funciona + +**Possíveis causas**: + +1. **Endereço incorreto**: Verificar se `transaction.predicateAddress` existe +2. **Redis não acessível**: Verificar conexão +3. **Erro silencioso**: Verificar logs de erro + +**Verificar**: +```typescript +// Adicionar logs +console.log('[INVALIDATE] Address:', predicateAddress); +console.log('[INVALIDATE] Flag set:', `balance:invalidated:${predicateAddress}`); + +// Verificar se flag foi criada +const flag = await RedisReadClient.get(`balance:invalidated:${predicateAddress}`); +console.log('[INVALIDATE] Flag exists:', !!flag); +``` + +### 4. Warm-up Não Executa + +**Possíveis causas**: + +1. **Promise não tratada**: Verificar se `.catch()` está presente +2. **Erro no warmupUserBalances()**: Verificar logs de erro +3. **Workspaces/Predicates não encontrados**: Verificar queries + +**Debug**: +```typescript +// Adicionar logs no início e fim +async warmupUserBalances(userId: string, network: Network) { + console.log('[WARMUP] START for user:', userId); + try { + // ... código ... + console.log('[WARMUP] END for user:', userId); + } catch (error) { + console.error('[WARMUP] FAILED for user:', userId, error); + } +} +``` + +### 5. Performance Não Melhorou + +**Checklist**: + +- [ ] Verificar se ProviderWithCache está sendo usado (logs de cache HIT/MISS) +- [ ] Confirmar que cache do Fuel SDK foi desabilitado (`resourceCacheTTL: 0`) +- [ ] Verificar se índices do banco foram criados (migration rodou?) +- [ ] Confirmar que warm-up está executando (logs de WARMUP) +- [ ] Medir latência individual de cada chamada + +**Métricas para coletar**: +```typescript +// Adicionar timing em endpoints críticos +console.time('[ENDPOINT] allocation'); +const result = await predicateService.allocation(...); +console.timeEnd('[ENDPOINT] allocation'); +``` + +### 6. Endpoints de Debug + +Adicionar endpoints temporários para diagnóstico: + +```typescript +// packages/api/src/modules/webhook/routes.ts + +// Ver stats do cache +router.get('/internal/cache-stats', async (req, res) => { + const balanceCache = App.getInstance()._balanceCache; + const stats = await balanceCache.stats(); + + // Buscar keys do Redis + // (precisa adicionar método no BalanceCache) + + return res.json({ + ...stats, + timestamp: Date.now(), + }); +}); + +// Forçar warm-up manual +router.post('/internal/warmup-user', async (req, res) => { + const { userId, networkUrl } = req.body; + + const provider = await FuelProvider.create(networkUrl); + const chainId = await provider.getChainId(); + + // Executar warm-up + // ... código do warmupUserBalances ... + + return res.json({ success: true }); +}); + +// Invalidar cache manualmente +router.post('/internal/invalidate-all', async (req, res) => { + // Limpar todo o cache (usar com cuidado!) + const balanceCache = App.getInstance()._balanceCache; + + // Implementar método clearAll() no BalanceCache se necessário + + return res.json({ success: true }); +}); +``` + +### 7. Monitoramento em Produção + +**Métricas importantes**: + +```typescript +// Adicionar ao monitoring +const cacheMetrics = { + hits: 0, + misses: 0, + invalidations: 0, + warmups: 0, +}; + +// Atualizar em cada operação +cacheMetrics.hits++; +cacheMetrics.misses++; + +// Expor via endpoint +router.get('/metrics', (req, res) => { + res.json({ + cache: cacheMetrics, + timestamp: Date.now(), + }); +}); +``` + +**Alertas sugeridos**: +- Cache hit rate < 70% (pode indicar TTL muito curto) +- Invalidações > 1000/hora (muitas transações) +- Warm-up failures > 10% (problema com blockchain) + +--- + +## Considerações de Segurança + +### 1. Endpoint de Invalidação + +O endpoint `/internal/invalidate-balance` deve ser protegido: + +```typescript +// Opção 1: IP whitelist +const ALLOWED_IPS = [ + '127.0.0.1', + 'worker-service-ip', +]; + +router.post('/internal/invalidate-balance', (req, res, next) => { + const clientIP = req.ip || req.connection.remoteAddress; + + if (!ALLOWED_IPS.includes(clientIP)) { + return res.status(403).json({ error: 'Forbidden' }); + } + + next(); +}, async (req, res) => { + // ... código de invalidação ... +}); + +// Opção 2: API Key +const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY; + +router.post('/internal/invalidate-balance', (req, res, next) => { + const apiKey = req.headers['x-api-key']; + + if (apiKey !== INTERNAL_API_KEY) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + next(); +}, async (req, res) => { + // ... código de invalidação ... +}); +``` + +### 2. Rate Limiting + +Proteger endpoints de invalidação contra abuse: + +```typescript +// Usar express-rate-limit +import rateLimit from 'express-rate-limit'; + +const invalidationLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minuto + max: 100, // Máximo 100 invalidações por minuto + message: 'Too many invalidation requests', +}); + +router.post( + '/internal/invalidate-balance', + invalidationLimiter, + async (req, res) => { + // ... código ... + } +); +``` + +--- + +## Integração com Worker (Fase Futura) + +### Worker → API Invalidation + +**Arquivo**: `packages/worker/src/queues/predicateBalance/queue.ts` + +```typescript +balanceQueue.process(async (job) => { + const db = await MongoDatabase.connect(); + const { predicate_address } = job.data; + + // ... processamento existente ... + + try { + await syncBalance(deposits, balance_collection, assets, price_collection); + + // Invalidar cache na API se houve mudanças + if (deposits.length > 0) { + try { + const API_URL = process.env.API_URL || 'http://localhost:3000'; + const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY; + + await fetch(`${API_URL}/internal/invalidate-balance`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': INTERNAL_API_KEY, + }, + body: JSON.stringify({ predicate_address }), + }); + + console.log(`[WORKER] Cache invalidated for ${predicate_address}`); + } catch (error) { + console.error('[WORKER] Failed to invalidate cache:', error); + // Não bloqueia processamento do worker + } + } + + return `Processed ${deposits.length} deposits for ${predicate_address}`; + } catch (e) { + console.error(e); + throw e; + } +}); +``` + +--- + +## Rollback Strategy + +Se houver problemas após deploy, rollback é simples: + +### Opção 1: Feature Flag + +```typescript +// packages/api/src/utils/config.ts +export const ENABLE_BALANCE_CACHE = process.env.ENABLE_BALANCE_CACHE === 'true'; + +// packages/api/src/utils/FuelProvider.ts +static async create(url: string, options?: ProviderOptions): Promise { + // ... código ... + + // Usar ProviderWithCache apenas se feature flag ativa + if (ENABLE_BALANCE_CACHE) { + const p = new ProviderWithCache(cleanUrl, providerOptions); + await p.connect(); + return p; + } + + // Fallback para Provider normal + const p = await Provider.create(cleanUrl, providerOptions); + return p; +} +``` + +### Opção 2: Rollback Total + +1. Reverter commits da implementação +2. Ou simplesmente setar `resourceCacheTTL: 0` no ProviderWithCache para desabilitar + +```typescript +// Desabilitar cache temporariamente sem mudar código +export class ProviderWithCache extends Provider { + async getBalances(address: string): Promise<{ balances: CoinQuantity[] }> { + // Bypass do cache - usar direto a blockchain + return super.getBalances(address); + } +} +``` + +--- + +## FAQ + +### P: O cache funciona com múltiplas instâncias da API? + +**R**: Sim! O cache está no Redis, que é compartilhado entre todas as instâncias. Todas as instâncias consultam o mesmo Redis e compartilham o mesmo cache. + +### P: O que acontece se Redis cair? + +**R**: O código tem fallback automático. Se Redis não estiver disponível, `BalanceCache.get()` retorna `null` e o sistema busca da blockchain normalmente. A aplicação continua funcionando, apenas mais lenta. + +### P: Quanto espaço no Redis será usado? + +**R**: Aproximadamente: +- **1 predicate** = ~1-5 KB (dependendo do número de assets) +- **100 predicates** = ~100-500 KB +- **1000 predicates** = ~1-5 MB + +Com TTL de 10 minutos, o espaço usado é limitado. + +### P: Como limpar todo o cache manualmente? + +**R**: +```bash +# Via Redis CLI +redis-cli KEYS "balance:*" | xargs redis-cli DEL + +# Ou via endpoint (se implementado) +curl -X POST http://localhost:3000/internal/clear-all-cache \ + -H "X-API-Key: seu-api-key" +``` + +### P: O warm-up consome muitos recursos? + +**R**: O warm-up: +- Roda em **background** (não bloqueia login) +- Usa **Promise.allSettled** (falhas não param o processo) +- Executa **apenas no login** (não periodicamente) +- É **opcional** (sistema funciona sem ele, só é mais lento) + +Se houver muitos logins simultâneos, considere: +- Rate limiting no warm-up +- Queue de warm-up (processar gradualmente) +- Desabilitar warm-up em horários de pico + +### P: Balances podem ficar desatualizados? + +**R**: Sim, mas isso é aceitável: +- **TTL de 10 minutos**: Dados no máximo 10 minutos desatualizados +- **Invalidação proativa**: Quando transação é confirmada, invalida imediatamente +- **Warm-up**: Busca dados frescos no login + +Para dados críticos (ex: antes de enviar transação), sempre força refresh: +```typescript +const provider = await FuelProvider.create(network.url); +// Força busca da blockchain +const balances = await provider.getBalances(address); +``` + +### P: Como monitorar a eficácia do cache? + +**R**: Adicionar métricas: +```typescript +// Incrementar contadores em cada operação +cache_hits_total +cache_misses_total +cache_invalidations_total +cache_warmup_duration_seconds + +// Calcular hit rate +hit_rate = cache_hits / (cache_hits + cache_misses) +``` + +Expor via Prometheus ou endpoint `/metrics`. + +--- + +## Próximos Passos + +Após implementação completa da Fase 1 (Cache de Balances): + +### Fase 2: Otimizações Adicionais + +1. **Endpoint Agregado**: Criar `GET /api/user/dashboard` que retorna todos os dados necessários em uma única chamada +2. **Eager Loading**: Otimizar queries SQL com joins para reduzir round-trips ao banco +3. **Índices Adicionais**: Adicionar índices em colunas frequentemente filtradas + +### Fase 3: Progressive Loading + +1. **Skeleton UI**: Frontend mostra UI parcial enquanto carrega dados pesados +2. **Server-Sent Events**: Stream de updates de balances conforme vão sendo calculados +3. **Lazy Loading**: Carregar dados menos importantes sob demanda + +### Fase 4: Worker Integration + +1. **Indexer de Balances**: Worker mantém cópia dos balances no MongoDB +2. **Change Data Capture**: Worker notifica API sobre mudanças em tempo real +3. **Fallback Híbrido**: Usar MongoDB como cache primário, Redis como cache secundário + +--- + +## Referências + +- **Fuel SDK Documentation**: https://docs.fuel.network/docs/fuels-ts/ +- **TypeORM Migrations**: https://typeorm.io/migrations +- **Redis Commands**: https://redis.io/commands +- **PostgreSQL Indexes**: https://www.postgresql.org/docs/current/indexes.html + +--- + +## Changelog + +### v1.0.0 - Initial Implementation +- Implementação de ProviderWithCache +- BalanceCache com Redis +- Invalidação em TransactionService e TransactionController +- Warm-up no AuthController +- Endpoint /internal/invalidate-balance + +### v1.1.0 - Performance Improvements (Planejado) +- Índices de banco de dados +- Endpoint agregado de dashboard +- Métricas de monitoramento + +### v2.0.0 - Worker Integration (Planejado) +- Indexer de balances no Worker +- Cache híbrido (MongoDB + Redis) +- Change Data Capture + +--- + +**Documento criado em**: 2025-01-25 +**Última atualização**: 2025-01-25 +**Autor**: Time Bsafe API +**Status**: Pronto para implementação diff --git a/packages/api/Dockerfile b/packages/api/Dockerfile index fcf0a34dd..f287d8fe5 100644 --- a/packages/api/Dockerfile +++ b/packages/api/Dockerfile @@ -1,23 +1,44 @@ -FROM arm64v8/node:18.18.2-alpine - -# Install dependencies necessary for node-gyp -RUN apk add --no-cache python3 make g++ \ - && npm install -g node-gyp +# syntax=docker/dockerfile:1.4 +FROM node:22-bookworm AS builder +# Bookworm already has python3/make/g++ - no need to install +# Build trigger: 2024-11-27 # Install pnpm globally RUN npm install -g pnpm # Create the application directory WORKDIR /api -# Add the application content to the working directory -ADD . /api +# Copy only package.json first (better layer caching) +COPY package.json ./ + +# Install dependencies with cache mount for pnpm store +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ + pnpm install + +# Copy source code after dependencies are installed +COPY . . + +# Build the application (excluding tests) +RUN pnpm build:prod + +# Production stage - smaller final image +FROM node:22-alpine AS production + +# Install pnpm globally +RUN npm install -g pnpm + +WORKDIR /api + +# Copy package.json +COPY package.json ./ -# Install the application dependencies using pnpm -RUN pnpm install +# Install only production dependencies with cache mount +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \ + pnpm install --prod -# Build the application -RUN pnpm build +# Copy built application from builder stage +COPY --from=builder /api/build ./build # Expose the application port EXPOSE 3333 diff --git a/packages/api/Makefile b/packages/api/Makefile index 1ab79d472..c8e222e7c 100644 --- a/packages/api/Makefile +++ b/packages/api/Makefile @@ -1,17 +1,19 @@ +export DOCKER_API_VERSION ?= 1.44 + deploy-prod: - docker-compose -f docker-compose.yml --env-file ${env_file} up --build -d > /dev/null 2>&1 + docker compose -f docker-compose.yml --env-file ${env_file} up --build -d > /dev/null 2>&1 deploy-stg: - docker-compose -f docker-compose.yml --env-file ${env_file} up --build -d > /dev/null 2>&1 + docker compose -f docker-compose.yml --env-file ${env_file} up --build -d > /dev/null 2>&1 deploy-test: - docker-compose -f docker-compose.yml --env-file ${env_file} up --build -d > /dev/null 2>&1 + docker compose -f docker-compose.yml --env-file ${env_file} up --build -d > /dev/null 2>&1 database-init: - docker-compose -f docker/database/docker-compose.yml --env-file ${env_file} up --build -d + docker compose -f docker/database/docker-compose.yml --env-file ${env_file} up --build -d database-down: - docker-compose -f docker/database/docker-compose.yml --env-file ${env_file} down + docker compose -f docker/database/docker-compose.yml --env-file ${env_file} down chain-start: - docker-compose -f docker/chain/docker-compose.yml --env-file ${env_file} up --build -d \ No newline at end of file + docker compose -f docker/chain/docker-compose.yml --env-file ${env_file} up --build -d \ No newline at end of file diff --git a/packages/api/doc/cache.md b/packages/api/doc/cache.md new file mode 100644 index 000000000..db4980c76 --- /dev/null +++ b/packages/api/doc/cache.md @@ -0,0 +1,266 @@ +# Cache Strategy Documentation + +This document describes the caching strategies implemented in the Bsafe API to optimize performance and reduce blockchain RPC calls. + +## Overview + +The API implements Redis-based caching for two main resources: +1. **Balance Cache** - Caches wallet balances per predicate +2. **Transaction Cache** - Caches confirmed transactions (deposits) from the Fuel blockchain + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ API Request │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Redis Cache Layer │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Balance Cache │ │ Transaction │ │ Session/Quote │ │ +│ │ (TTL: 5min) │ │ Cache (10min) │ │ Cache │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ (on cache miss) +┌─────────────────────────────────────────────────────────────────┐ +│ Fuel Blockchain (RPC) │ +│ - getBalances() │ +│ - getTransactionsSummaries() │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## 1. Balance Cache + +### Purpose +Caches `provider.getBalances()` results to reduce RPC calls. Wallet balances don't change frequently unless a transaction occurs. + +### Key Structure +``` +balance:{predicateAddress}:{chainId} +``` + +### Configuration +| Variable | Default | Description | +|----------|---------|-------------| +| `ENABLE_BALANCE_CACHE` | `true` | Enable/disable balance cache | +| `BALANCE_CACHE_TTL` | `300` | Cache TTL in seconds (5 minutes) | +| `BALANCE_INVALIDATION_TTL` | `3600` | Invalidation flag TTL (1 hour) | + +### Features +- **Automatic invalidation**: Cache is invalidated when a transaction is confirmed +- **Granular invalidation**: Invalidates only the affected chain, not all chains +- **BigNumber serialization**: Properly handles Fuel's BigNumber format + +### Invalidation Triggers +- Transaction confirmed (`TransactionService.sendToChain`) +- Transaction closed (`TransactionController.close`) + +--- + +## 2. Transaction Cache + +### Purpose +Caches confirmed transactions (deposits) from the Fuel blockchain. Since confirmed transactions are **immutable**, they can be cached for longer periods. + +### Key Structure +``` +tx:{predicateAddress}:{chainId} # Transaction data +tx:refresh:{predicateAddress}:{chainId} # Refresh flag +``` + +### Configuration +| Variable | Default | Description | +|----------|---------|-------------| +| `TRANSACTION_CACHE_TTL` | `600` | Cache TTL in seconds (10 minutes) | +| `TRANSACTION_INCREMENTAL_LIMIT` | `10` | Number of recent txs to fetch on refresh | + +### Incremental Refresh Strategy + +Instead of invalidating and refetching all transactions, the cache uses an **incremental refresh** approach: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Normal Cache Hit │ +│ 1. Check cache exists │ +│ 2. Check refresh flag NOT set │ +│ 3. Return cached transactions │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ Incremental Refresh (on invalidation) │ +│ 1. Check cache exists │ +│ 2. Check refresh flag IS set │ +│ 3. Fetch only last N transactions (default: 10) │ +│ 4. Merge new txs with cached (deduplicate by hash) │ +│ 5. Update cache with merged data │ +│ 6. Clear refresh flag │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ Full Fetch (cache miss) │ +│ 1. No cache exists │ +│ 2. Fetch all transactions (up to 57) │ +│ 3. Store in cache │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Why Incremental? + +| Scenario | Full Invalidation | Incremental Refresh | +|----------|-------------------|---------------------| +| Cache HIT | ~5ms | ~5ms | +| After invalidation | ~1000ms (57 txs) | ~200ms (10 txs) | +| Data freshness | Stale until refetch | Always fresh on next request | + +Since confirmed transactions are immutable, we only need to fetch **new** transactions and merge them with the cached ones. + +### Deduplication + +Transactions are deduplicated by their `hash` or `id`: + +```typescript +// Known hashes stored in cache +knownHashes: ['0xabc...', '0xdef...', ...] + +// New transactions filtered +newTxs.filter(tx => !knownHashes.has(tx.hash)) +``` + +--- + +## 3. Warm-up Strategy + +On user login, the API pre-warms the balance cache for the user's predicates. + +### Configuration +| Variable | Default | Description | +|----------|---------|-------------| +| `WARMUP_ENABLED` | `true` | Enable/disable warmup | +| `WARMUP_CONCURRENCY` | `5` | Max concurrent balance fetches | +| `WARMUP_MAX_PREDICATES` | `20` | Max predicates to warm per user | +| `WARMUP_SKIP_CACHED` | `true` | Skip predicates already in cache | + +### Optimization Details +- Orders predicates by `updatedAt DESC` (most recently used first) +- Limits to `maxPredicates` per warmup +- Checks if cache already exists before fetching +- Uses global `chainId` cache to avoid extra RPC calls + +--- + +## 4. Global ChainId Cache + +The `FuelProvider` maintains a global cache of `chainId` per provider URL to avoid repeated `getChainId()` calls. + +```typescript +FuelProvider.getChainId(url) // Uses cache or fetches once +``` + +--- + +## 5. Internal Endpoints + +For debugging and management, the following internal endpoints are available: + +### Balance Cache +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/internal/cache/stats` | GET | Cache statistics | +| `/internal/cache/keys` | GET | List cache keys | +| `/internal/cache/invalidate` | POST | Manually invalidate cache | +| `/internal/cache/warmup` | POST | Trigger manual warmup | +| `/internal/cache/metrics/reset` | POST | Reset metrics | + +### Request Examples + +```bash +# Get cache stats +curl http://localhost:3333/internal/cache/stats + +# Invalidate specific predicate +curl -X POST http://localhost:3333/internal/cache/invalidate \ + -H "Content-Type: application/json" \ + -d '{"predicateAddress": "0x123..."}' + +# Warmup for user +curl -X POST http://localhost:3333/internal/cache/warmup \ + -H "Content-Type: application/json" \ + -d '{"userId": "user-uuid", "networkUrl": "https://..."}' +``` + +--- + +## 6. Metrics + +Cache performance is tracked via `CacheMetrics`: + +```typescript +CacheMetrics.hit() // Increment hit counter +CacheMetrics.miss() // Increment miss counter +CacheMetrics.error() // Increment error counter +CacheMetrics.warmup(count) // Track warmup operations +``` + +Access via `/internal/cache/stats`: +```json +{ + "hits": 1234, + "misses": 56, + "errors": 2, + "warmups": 100, + "hitRate": "95.67%" +} +``` + +--- + +## 7. Best Practices + +### When to Invalidate +- After any transaction state change (confirmed, closed) +- When predicate configuration changes +- On explicit user request (force refresh) + +### When NOT to Invalidate +- On read operations +- On transaction creation (before confirmation) +- On signature additions (transaction still pending) + +### TTL Guidelines +| Resource | Volatility | Recommended TTL | +|----------|------------|-----------------| +| Balances | Medium (changes on tx) | 5 minutes | +| Confirmed Txs | None (immutable) | 10+ minutes | +| Quotes | High | 1-5 minutes | +| Sessions | Low | 40 minutes | + +--- + +## 8. Troubleshooting + +### High Cache Miss Rate +1. Check TTL settings - may be too short +2. Verify invalidation isn't being triggered too often +3. Check Redis connection health + +### Stale Data +1. Verify invalidation triggers are working +2. Check if refresh flags are being set correctly +3. Review transaction confirmation flow + +### Memory Issues +1. Monitor Redis memory usage +2. Consider reducing TTLs +3. Implement cache eviction policies if needed + +--- + +## 9. Future Improvements + +- [ ] Add cache compression for large transaction lists +- [ ] Implement cache warming on deployment +- [ ] Add distributed cache invalidation (pub/sub) +- [ ] Implement circuit breaker for Redis failures diff --git a/packages/api/docker-compose.yml b/packages/api/docker-compose.yml index c88dca63f..4b69673af 100644 --- a/packages/api/docker-compose.yml +++ b/packages/api/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: api: container_name: ${API_NAME} diff --git a/packages/api/package.json b/packages/api/package.json index 864611eb3..2010f8e1f 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -5,8 +5,13 @@ "license": "Apache-2.0", "scripts": { "dev": "NODE_ENV=development ts-node-dev --respawn --transpile-only -r tsconfig-paths/register -r dotenv/config src/server/index.ts", + "api:dev:start": "wait-on tcp:5432 tcp:6379 tcp:4000 tcp:3001 -t 60000 && pnpm dev", "start": "node ./build/server/index.js", - "build": "tsc --project . && tscpaths -p tsconfig.json -s ./src -o ./build", + "build": "tsc --project . && tsc-alias -p tsconfig.json", + "typecheck": "tsc --noEmit", + "lint": "eslint src --ext .ts", + "lint:fix": "eslint src --ext .ts --fix", + "build:prod": "tsc --project tsconfig.build.json && tsc-alias -p tsconfig.build.json && pnpm run postbuild", "copyFiles": "copyfiles --error --up 1 src/**/*.html build", "postbuild": "pnpm run copyFiles", "migration:run": "ts-node -r tsconfig-paths/register -r dotenv/config ./node_modules/typeorm/cli.js migration:run --dataSource src/database", @@ -15,7 +20,7 @@ "run:prod": "make -C ./ deploy-prod env_file=.env.prod", "run:stg": "make -C ./ deploy-stg env_file=.env.staging", "run:test": "make -C ./ deploy-test env_file=.env.test", - "test:build": "pnpm build > /dev/null 2>&1 && pnpm copy:predicate-releases && cross-env TESTCONTAINERS_DB=true node --test build/tests/*.tests.js", + "test:build": "pnpm build > /dev/null 2>&1 && pnpm copy:predicate-releases && cross-env TESTCONTAINERS_DB=true node --test-force-exit --test build/tests/*.tests.js", "database:populate": "chmod +x ./src/scripts/db-populate.sh && ./src/scripts/db-populate.sh", "database:clear": "chmod +x ./src/scripts/db-clear.sh && ./src/scripts/db-clear.sh", "copy:predicate-releases": "cp -r src/tests/mocks/predicate-release build/tests/mocks/" @@ -24,7 +29,7 @@ "@ethereumjs/util": "9.0.3", "@noble/curves": "1.3.0", "@opentelemetry/api": "1.9.0", - "@opentelemetry/exporter-trace-otlp-proto": "0.201.1", + "@opentelemetry/exporter-trace-otlp-proto": "0.212.0", "@opentelemetry/instrumentation": "0.201.1", "@opentelemetry/instrumentation-express": "0.50.0", "@opentelemetry/instrumentation-http": "0.201.1", @@ -37,9 +42,9 @@ "@sentry/node": "8.32.0", "@sentry/profiling-node": "8.32.0", "@testcontainers/postgresql": "11.0.0", - "axios": "1.5.1", - "bakosafe": "0.2.2", - "body-parser": "1.20.2", + "axios": "1.13.5", + "bakosafe": "0.6.0", + "body-parser": "1.20.4", "cheerio": "1.0.0-rc.12", "class-validator": "0.14.0", "cookie-parser": "1.4.6", @@ -47,39 +52,42 @@ "cors": "2.8.5", "date-fns": "2.30.0", "dotenv": "16.4.5", - "express": "4.17.1", + "express": "4.21.2", "express-joi-validation": "5.0.0", "fuels": "0.101.3", - "glob": "10.3.15", + "glob": "10.5.0", "handlebars": "4.7.8", "joi": "17.4.0", - "jsonwebtoken": "9.0.1", + "jsonwebtoken": "9.0.3", "morgan": "1.10.0", "node-cron": "3.0.3", - "nodemailer": "6.9.8", + "nodemailer": "8.0.1", "patch-package": "8.0.0", "pg": "8.5.1", + "pino": "9.6.0", "qs": "6.12.1", "redis": "4.7.0", "reflect-metadata": "0.1.13", "socket.io": "4.7.2", "socket.io-client": "4.7.5", + "svix": "1.76.1", "ts-node": "10.9.2", "tsconfig-paths": "3.15.0", - "typeorm": "0.3.20", + "typeorm": "0.3.28", "typescript": "~5.4.5" }, "devDependencies": { "@commitlint/cli": "12.0.1", "@commitlint/config-conventional": "12.0.1", - "@trivago/prettier-plugin-sort-imports": "2.0.2", + "@trivago/prettier-plugin-sort-imports": "4.3.0", "@types/cors": "2.8.10", "@types/express": "4.17.11", "@types/glob": "8.1.0", - "@types/jsonwebtoken": "9.0.2", + "@types/jsonwebtoken": "9.0.10", "@types/morgan": "1.9.2", "@types/node": "20.6.0", "@types/node-cron": "3.0.11", + "@types/qs": "6.14.0", "@types/supertest": "2.0.10", "@typescript-eslint/eslint-plugin": "6.5.0", "@typescript-eslint/parser": "6.5.0", @@ -90,12 +98,14 @@ "eslint-plugin-prettier": "3.3.1", "husky": "5.2.0", "lint-staged": "10.5.4", + "pino-pretty": "11.2.2", "prettier": "2.2.1", "pretty-quick": "3.1.0", "supertest": "6.1.3", - "ts-node-dev": "1.1.6", + "ts-node-dev": "2.0.0", + "tsc-alias": "1.8.16", "tscpaths": "0.0.9", - "tsx": "4.19.3", + "tsx": "4.21.0", "wait-on": "8.0.3", "why-is-node-running": "3.2.2" }, diff --git a/packages/api/src/config/cache.ts b/packages/api/src/config/cache.ts new file mode 100644 index 000000000..2213b15b3 --- /dev/null +++ b/packages/api/src/config/cache.ts @@ -0,0 +1,154 @@ +import { RedisReadClient } from '@src/utils/redis/RedisReadClient'; +import { logger } from '@src/config/logger'; +import { RedisWriteClient } from '@src/utils/redis/RedisWriteClient'; + +/** + * Cache configuration for Balance Cache feature + * All values are configurable via environment variables + */ + +export const cacheConfig = { + // Feature flags + enabled: process.env.ENABLE_BALANCE_CACHE !== 'false', // enabled by default + + // Cache TTL in seconds (default: 5 minutes) + ttl: parseInt(process.env.BALANCE_CACHE_TTL || '300', 10), + + // Invalidation flag TTL in seconds (default: 1 hour) + invalidationFlagTtl: parseInt(process.env.BALANCE_INVALIDATION_TTL || '3600', 10), + + // Warm-up configuration + warmup: { + enabled: process.env.WARMUP_ENABLED !== 'false', // enabled by default + concurrency: parseInt(process.env.WARMUP_CONCURRENCY || '5', 10), + maxPredicates: parseInt(process.env.WARMUP_MAX_PREDICATES || '20', 10), // Limit predicates per warmup + skipIfCached: process.env.WARMUP_SKIP_CACHED !== 'false', // Skip if already cached + }, + + // Redis key prefixes + prefixes: { + balance: 'balance', + invalidated: 'balance:invalidated', + metrics: 'cache:metrics', + }, +}; + +const METRICS_KEY = cacheConfig.prefixes.metrics; + +/** + * Cache metrics singleton for monitoring cache performance + * Stores metrics in Redis for persistence across restarts and memory efficiency + */ +class CacheMetricsClass { + private startTime = Date.now(); + + /** + * Increment a metric field in Redis + */ + private async increment(field: string, count = 1): Promise { + try { + const current = await RedisReadClient.get(`${METRICS_KEY}:${field}`); + const newValue = (parseInt(current || '0', 10) + count).toString(); + await RedisWriteClient.set(`${METRICS_KEY}:${field}`, newValue); + } catch (error) { + // Silently fail - metrics should not break the app + logger.error({ field, error }, '[CacheMetrics] Error incrementing:'); + } + } + + /** + * Get a metric value from Redis + */ + private async getValue(field: string): Promise { + try { + const value = await RedisReadClient.get(`${METRICS_KEY}:${field}`); + return parseInt(value || '0', 10); + } catch { + return 0; + } + } + + hit(): void { + this.increment('hits').catch(() => {}); + } + + miss(): void { + this.increment('misses').catch(() => {}); + } + + invalidate(count = 1): void { + this.increment('invalidations', count).catch(() => {}); + } + + warmup(predicateCount = 0): void { + this.increment('warmups').catch(() => {}); + if (predicateCount > 0) { + this.increment('warmupPredicates', predicateCount).catch(() => {}); + } + } + + error(): void { + this.increment('errors').catch(() => {}); + } + + async getStats(): Promise { + const [ + hits, + misses, + invalidations, + warmups, + warmupPredicates, + errors, + ] = await Promise.all([ + this.getValue('hits'), + this.getValue('misses'), + this.getValue('invalidations'), + this.getValue('warmups'), + this.getValue('warmupPredicates'), + this.getValue('errors'), + ]); + + const total = hits + misses; + const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1000); + + return { + hits, + misses, + hitRate: total > 0 ? Math.round((hits / total) * 10000) / 100 : 0, + invalidations, + warmups, + warmupPredicates, + errors, + uptimeSeconds, + }; + } + + async reset(): Promise { + try { + await RedisWriteClient.del([ + `${METRICS_KEY}:hits`, + `${METRICS_KEY}:misses`, + `${METRICS_KEY}:invalidations`, + `${METRICS_KEY}:warmups`, + `${METRICS_KEY}:warmupPredicates`, + `${METRICS_KEY}:errors`, + ]); + this.startTime = Date.now(); + } catch (error) { + logger.error({ error: error }, '[CacheMetrics] Error resetting:'); + } + } +} + +export interface CacheStats { + hits: number; + misses: number; + hitRate: number; + invalidations: number; + warmups: number; + warmupPredicates: number; + errors: number; + uptimeSeconds: number; +} + +export const CacheMetrics = new CacheMetricsClass(); diff --git a/packages/api/src/config/logger.ts b/packages/api/src/config/logger.ts new file mode 100644 index 000000000..bb9d2389e --- /dev/null +++ b/packages/api/src/config/logger.ts @@ -0,0 +1,114 @@ +import pino from 'pino'; + +const { NODE_ENV, LOG_LEVEL } = process.env; + +const isDevelopment = NODE_ENV === 'development'; + +const pinoConfig: pino.LoggerOptions = { + level: LOG_LEVEL || (isDevelopment ? 'debug' : 'info'), + timestamp: pino.stdTimeFunctions.isoTime, + + // Sensitive data redaction for LGPD, GDPR, PCI DSS, SOC 2 Type II compliance + redact: { + paths: [ + // ===== Authentication & Authorization ===== + 'password', + '*.password', + 'token', + '*.token', + 'authorization', + '*.authorization', + 'headers.authorization', + 'apiKey', + '*.apiKey', + 'api_key', + '*.api_key', + 'accessToken', + '*.accessToken', + 'refreshToken', + '*.refreshToken', + + // ===== Cryptography & Keys (12 terms) ===== + 'privateKey', + '*.privateKey', + 'private_key', + '*.private_key', + 'seed', + '*.seed', + 'mnemonic', + '*.mnemonic', + 'signature', + '*.signature', + 'signedMessage', + '*.signedMessage', + + // ===== Blockchain & Fuel (10 terms) ===== + 'wallet', + '*.wallet', + 'walletAddress', + '*.walletAddress', + 'predicateAddress', + '*.predicateAddress', + 'vault.configurable', + 'signer', + '*.signer', + 'predicate_address', + + // ===== WebAuthn & Hardware Security (8 terms) ===== + 'webauthn', + '*.webauthn', + 'credentialId', + '*.credentialId', + 'credential_id', + '*.credential_id', + 'credentialPublicKey', + '*.credentialPublicKey', + + // ===== Infrastructure & Endpoints (10 terms) ===== + 'DATABASE_URL', + 'REDIS_URL', + 'connectionString', + '*.connectionString', + 'connection_string', + + // ===== User Data & Recovery (14 terms) ===== + 'code', + '*.code', + 'recovery_code', + '*.recovery_code', + 'pin', + '*.pin', + 'email', + '*.email', + 'phone', + '*.phone', + + // ===== Transaction & Operation Data (11 terms) ===== + 'operationData', + '*.operationData', + 'operation_data', + '*.operation_data', + + // ===== Network & Connectivity (3 terms) ===== + 'ipAddress', + 'ip_address', + ], + remove: true, + }, + + // Development: human-readable output with pino-pretty + // Production: JSON structured logging for centralized systems + transport: isDevelopment + ? { + target: 'pino-pretty', + options: { + colorize: true, + singleLine: false, + translateTime: 'SYS:standard', + ignore: 'pid,hostname', + }, + } + : undefined, +}; + +export const logger = pino(pinoConfig); diff --git a/packages/api/src/constants/assets.ts b/packages/api/src/constants/assets.ts new file mode 100644 index 000000000..1e0f871d9 --- /dev/null +++ b/packages/api/src/constants/assets.ts @@ -0,0 +1,13 @@ +import { hashMessage } from 'fuels'; + +export const ASSETS = { + FUEL_ETH: '0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07', +}; + +// create a hashMessage for fiat currencies +// The keys are the currency codes, and the values are the asset IDs or other identifiers. +export const FIAT_CURRENCIES = { + USD: hashMessage('USD'), + EUR: hashMessage('EUR'), + BRL: hashMessage('BRL'), +}; diff --git a/packages/api/src/constants/networks.ts b/packages/api/src/constants/networks.ts index 904f22d63..6207b64e5 100644 --- a/packages/api/src/constants/networks.ts +++ b/packages/api/src/constants/networks.ts @@ -4,3 +4,8 @@ export const networks: { [key: string]: string } = { local: 'http://127.0.0.1:4000/v1/graphql', devnet: 'https://testnet.fuel.network/v1/graphql', }; + +export const networksByChainId: { [key: string]: string } = { + '0': 'https://testnet.fuel.network/v1/graphql', + '9889': 'https://mainnet.fuel.network/v1/graphql', +}; diff --git a/packages/api/src/database/index.ts b/packages/api/src/database/index.ts new file mode 100644 index 000000000..19fdb0cd3 --- /dev/null +++ b/packages/api/src/database/index.ts @@ -0,0 +1,8 @@ +import { DataSource } from 'typeorm'; +import { getDatabaseConfig } from '../config/database'; + +/** + * TypeORM DataSource for CLI commands (migrations, etc.) + * This is the entry point for `typeorm` CLI operations. + */ +export default new DataSource(getDatabaseConfig()); diff --git a/packages/api/src/database/migrations/1727715693680-add-network-on-transaction.ts b/packages/api/src/database/migrations/1727715693680-add-network-on-transaction.ts index b3166ecd6..111cc001d 100644 --- a/packages/api/src/database/migrations/1727715693680-add-network-on-transaction.ts +++ b/packages/api/src/database/migrations/1727715693680-add-network-on-transaction.ts @@ -1,3 +1,4 @@ +import 'dotenv/config'; import { Provider } from 'fuels'; import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; @@ -15,9 +16,10 @@ export class AddNetworkOnTransaction1727715693680 implements MigrationInterface ); const provider = new Provider(FUEL_PROVIDER); + await provider.init(); const network = { url: provider.url, - chainId: await provider.getChainId(), + chainId: await provider.getChainId().catch(() => 0), }; const networkString = JSON.stringify(network); diff --git a/packages/api/src/database/migrations/1727717621119-add-network-on-api-tokens.ts b/packages/api/src/database/migrations/1727717621119-add-network-on-api-tokens.ts index 172568ed3..2c48534ad 100644 --- a/packages/api/src/database/migrations/1727717621119-add-network-on-api-tokens.ts +++ b/packages/api/src/database/migrations/1727717621119-add-network-on-api-tokens.ts @@ -15,9 +15,10 @@ export class AddNetworkOnApiTokens1727717621119 implements MigrationInterface { ); const provider = new Provider(FUEL_PROVIDER); + await provider.init(); const network = { url: provider.url, - chainId: await provider.getChainId(), + chainId: await provider.getChainId().catch(() => 0), }; const networkString = JSON.stringify(network); diff --git a/packages/api/src/database/migrations/1750783310537-create-ramp-transactions-table.ts b/packages/api/src/database/migrations/1750783310537-create-ramp-transactions-table.ts new file mode 100644 index 000000000..9d878f969 --- /dev/null +++ b/packages/api/src/database/migrations/1750783310537-create-ramp-transactions-table.ts @@ -0,0 +1,74 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class CreateMeldTransactionsTable1750783310537 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'ramp_transactions', + columns: [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + isUnique: true, + generationStrategy: 'uuid', + default: `uuid_generate_v4()`, + }, + { + name: 'provider', + type: 'varchar', + isNullable: false, + }, + { + name: 'provider_data', + type: 'jsonb', + isNullable: true, + }, + { + name: 'user_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'transaction_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamp', + }, + { + name: 'updated_at', + type: 'timestamp', + isNullable: true, + }, + { + name: 'deleted_at', + type: 'timestamp', + isNullable: true, + }, + ], + foreignKeys: [ + { + columnNames: ['user_id'], + referencedTableName: 'users', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }, + { + columnNames: ['transaction_id'], + referencedTableName: 'transactions', + referencedColumnNames: ['id'], + onDelete: 'CASCADE', + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('ramp_transactions'); + } +} diff --git a/packages/api/src/database/migrations/1752097335860-update-ramp-transactions-table.ts b/packages/api/src/database/migrations/1752097335860-update-ramp-transactions-table.ts new file mode 100644 index 000000000..761fe4976 --- /dev/null +++ b/packages/api/src/database/migrations/1752097335860-update-ramp-transactions-table.ts @@ -0,0 +1,44 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class UpdateRampTransactionsTable1752097335860 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumns('ramp_transactions', [ + new TableColumn({ + name: 'source_currency', + type: 'varchar', + isNullable: true, + }), + new TableColumn({ + name: 'source_amount', + type: 'varchar', + isNullable: true, + }), + new TableColumn({ + name: 'destination_currency', + type: 'varchar', + isNullable: true, + }), + new TableColumn({ + name: 'destination_amount', + type: 'varchar', + isNullable: true, + }), + new TableColumn({ + name: 'payment_method', + type: 'varchar', + isNullable: true, + }), + ]); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumns('ramp_transactions', [ + 'source_currency', + 'source_amount', + 'destination_currency', + 'destination_amount', + 'payment_method', + ]); + } +} diff --git a/packages/api/src/database/migrations/1757622049546-add-user-wallet-address-to-ramp-transactions.ts b/packages/api/src/database/migrations/1757622049546-add-user-wallet-address-to-ramp-transactions.ts new file mode 100644 index 000000000..236535c54 --- /dev/null +++ b/packages/api/src/database/migrations/1757622049546-add-user-wallet-address-to-ramp-transactions.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddUserWalletAddressToRampTransactions1757622049546 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + 'ramp_transactions', + new TableColumn({ + name: 'user_wallet_address', + type: 'varchar', + isNullable: true, + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('ramp_transactions', 'user_wallet_address'); + } +} diff --git a/packages/api/src/database/migrations/1759241383524-add-is-sandbox-to-ramp-transaction.ts b/packages/api/src/database/migrations/1759241383524-add-is-sandbox-to-ramp-transaction.ts new file mode 100644 index 000000000..f428fe579 --- /dev/null +++ b/packages/api/src/database/migrations/1759241383524-add-is-sandbox-to-ramp-transaction.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddIsSandboxToRampTransaction1759241383524 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + 'ramp_transactions', + new TableColumn({ + name: 'is_sandbox', + type: 'boolean', + isNullable: false, + default: false, + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('ramp_transactions', 'is_sandbox'); + } +} diff --git a/packages/api/src/database/migrations/1764104869733-add-performance-indexes.ts b/packages/api/src/database/migrations/1764104869733-add-performance-indexes.ts new file mode 100644 index 000000000..1b860274e --- /dev/null +++ b/packages/api/src/database/migrations/1764104869733-add-performance-indexes.ts @@ -0,0 +1,38 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPerformanceIndexes1764104869733 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Índices para acelerar queries de predicates por usuário + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "idx_predicate_members_user" ON "predicate_members" ("user_id")`, + ); + + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "idx_predicates_owner" ON "predicates" ("owner_id")`, + ); + + // Índices para acelerar queries de transactions + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "idx_transactions_predicate_status" ON "transactions" ("predicate_id", "status")`, + ); + + // Índice para filtrar transactions por network URL (JSONB) + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "idx_transactions_network_url" ON "transactions" ((network->>'url'))`, + ); + + console.log('Performance indexes created successfully'); + } + + public async down(queryRunner: QueryRunner): Promise { + // Remover índices na ordem inversa + await queryRunner.query(`DROP INDEX IF EXISTS "idx_transactions_network_url"`); + await queryRunner.query( + `DROP INDEX IF EXISTS "idx_transactions_predicate_status"`, + ); + await queryRunner.query(`DROP INDEX IF EXISTS "idx_predicates_owner"`); + await queryRunner.query(`DROP INDEX IF EXISTS "idx_predicate_members_user"`); + + console.log('Performance indexes dropped successfully'); + } +} diff --git a/packages/api/src/database/migrations/1764177686000-add-pending-transactions-indexes.ts b/packages/api/src/database/migrations/1764177686000-add-pending-transactions-indexes.ts new file mode 100644 index 000000000..04f5aa252 --- /dev/null +++ b/packages/api/src/database/migrations/1764177686000-add-pending-transactions-indexes.ts @@ -0,0 +1,46 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Migration to optimize /transaction/pending endpoint + * + * Adds indexes for: + * - status filtering (partial index for AWAIT_REQUIREMENTS) + * - chainId lookup (instead of URL regex) + * - workspace lookup through predicate + */ +export class AddPendingTransactionsIndexes1764177686000 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Partial index for pending transactions only + // This is very efficient because it only indexes rows with this status + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_transactions_pending" + ON "transactions" ("predicate_id") + WHERE status = 'await_requirements' + `); + + // Index for chainId lookup (faster than URL regex) + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_transactions_network_chainid" + ON "transactions" ((network->>'chainId')) + `); + + // Composite index for workspace queries on predicates + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_predicates_workspace" + ON "predicates" ("workspace_id") + `); + + console.log('[Migration] Pending transactions indexes created'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX IF EXISTS "idx_transactions_pending"`); + await queryRunner.query( + `DROP INDEX IF EXISTS "idx_transactions_network_chainid"`, + ); + await queryRunner.query(`DROP INDEX IF EXISTS "idx_predicates_workspace"`); + + console.log('[Migration] Pending transactions indexes dropped'); + } +} diff --git a/packages/api/src/database/migrations/1764200000000-add-additional-performance-indexes.ts b/packages/api/src/database/migrations/1764200000000-add-additional-performance-indexes.ts new file mode 100644 index 000000000..2921f43ea --- /dev/null +++ b/packages/api/src/database/migrations/1764200000000-add-additional-performance-indexes.ts @@ -0,0 +1,59 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Migration to add additional performance indexes + * + * Adds indexes for: + * - predicates.predicate_address (frequent lookups by address) + * - predicates.workspace_id (workspace filtering) + * - transactions.created_by (user transaction history) + * - users.address (user lookups by address) + */ +export class AddAdditionalPerformanceIndexes1764200000000 + implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Index for predicate address lookups (findByAddress) + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_predicates_predicate_address" + ON "predicates" ("predicate_address") + `); + + // Index for workspace filtering on predicates + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_predicates_workspace_id" + ON "predicates" ("workspace_id") + `); + + // Index for user transaction history + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_transactions_created_by" + ON "transactions" ("created_by") + `); + + // Index for user lookups by address + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_users_address" + ON "users" ("address") + `); + + // Index for notification user filtering + await queryRunner.query(` + CREATE INDEX IF NOT EXISTS "idx_notifications_user_id" + ON "notifications" ("user_id") + `); + + console.log('[Migration] Additional performance indexes created'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX IF EXISTS "idx_notifications_user_id"`); + await queryRunner.query(`DROP INDEX IF EXISTS "idx_users_address"`); + await queryRunner.query(`DROP INDEX IF EXISTS "idx_transactions_created_by"`); + await queryRunner.query(`DROP INDEX IF EXISTS "idx_predicates_workspace_id"`); + await queryRunner.query( + `DROP INDEX IF EXISTS "idx_predicates_predicate_address"`, + ); + + console.log('[Migration] Additional performance indexes dropped'); + } +} diff --git a/packages/api/src/middlewares/auth/index.ts b/packages/api/src/middlewares/auth/index.ts index 438fb60ec..824cebc6a 100644 --- a/packages/api/src/middlewares/auth/index.ts +++ b/packages/api/src/middlewares/auth/index.ts @@ -27,9 +27,12 @@ async function authMiddleware( } const authStrategy = AuthStrategyFactory.createStrategy(signature); - const { user, workspace, network } = await authStrategy.authenticate(req); + const { user, workspace, network, dapp } = await authStrategy.authenticate(req); - if (address !== user.address) { + const isValidAddress = address === user.address; + const isValidPredicateAddress = address === dapp?.currentVault.predicateAddress; + + if (!isValidAddress && !isValidPredicateAddress) { throw new Unauthorized({ type: ErrorTypes.Unauthorized, title: UnauthorizedErrorTitles.INVALID_ADDRESS, @@ -40,6 +43,7 @@ async function authMiddleware( req.user = user; req.workspace = workspace; req.network = network; + req.dapp = dapp; return next(); } catch (e) { return next(e); diff --git a/packages/api/src/middlewares/auth/methods/index.ts b/packages/api/src/middlewares/auth/methods/index.ts index 673e5ff36..7c3c7ce12 100644 --- a/packages/api/src/middlewares/auth/methods/index.ts +++ b/packages/api/src/middlewares/auth/methods/index.ts @@ -2,6 +2,7 @@ import { CliAuthStrategy, CodeAuthStrategy, TokenAuthStrategy, + ConnectorAuthStrategy, AuthStrategy, } from './strategies'; @@ -15,6 +16,10 @@ export class AuthStrategyFactory { return new CodeAuthStrategy(); } + if (signature.startsWith('connector')) { + return new ConnectorAuthStrategy(); + } + return new TokenAuthStrategy(); } } diff --git a/packages/api/src/middlewares/auth/methods/strategies/connector.ts b/packages/api/src/middlewares/auth/methods/strategies/connector.ts new file mode 100644 index 000000000..545266447 --- /dev/null +++ b/packages/api/src/middlewares/auth/methods/strategies/connector.ts @@ -0,0 +1,54 @@ +import { IAuthRequest } from '@middlewares/auth/types'; +import { ErrorTypes, Unauthorized, UnauthorizedErrorTitles } from '@utils/error'; +import { DApp } from '@src/models'; +import { AuthStrategy } from './type'; + +export class ConnectorAuthStrategy implements AuthStrategy { + async authenticate(req: IAuthRequest) { + const sessionId = req?.headers?.authorization; + const predicateAddress = req?.headers?.signeraddress; + + if (!sessionId || !predicateAddress) { + throw new Unauthorized({ + type: ErrorTypes.Unauthorized, + title: UnauthorizedErrorTitles.MISSING_CREDENTIALS, + detail: 'SessionId and predicate address are required', + }); + } + + const dapp = await DApp.createQueryBuilder('d') + .innerJoin('d.currentVault', 'currentVault') + .addSelect(['currentVault.predicateAddress', 'currentVault.id']) + .innerJoinAndSelect('d.user', 'user') + .where('d.session_id = :sessionId', { + sessionId: sessionId.replace('connector', ''), + }) + .getOne(); + + if (!dapp) { + throw new Unauthorized({ + type: ErrorTypes.Unauthorized, + title: UnauthorizedErrorTitles.INVALID_CREDENTIALS, + detail: 'Invalid sessionId', + }); + } + + if ( + dapp.user.address !== predicateAddress && + dapp.currentVault.predicateAddress !== predicateAddress + ) { + throw new Unauthorized({ + type: ErrorTypes.Unauthorized, + title: UnauthorizedErrorTitles.INVALID_ADDRESS, + detail: 'Invalid predicate address for this session', + }); + } + + return { + user: dapp.user, + workspace: null, + network: dapp.network, + dapp: dapp, + }; + } +} diff --git a/packages/api/src/middlewares/auth/methods/strategies/index.ts b/packages/api/src/middlewares/auth/methods/strategies/index.ts index 4d766cc6f..ae1d72edf 100644 --- a/packages/api/src/middlewares/auth/methods/strategies/index.ts +++ b/packages/api/src/middlewares/auth/methods/strategies/index.ts @@ -1,3 +1,4 @@ export * from './token'; export * from './code'; +export * from './connector'; export * from './type'; diff --git a/packages/api/src/middlewares/auth/methods/strategies/token.ts b/packages/api/src/middlewares/auth/methods/strategies/token.ts index 373a5403e..21f416654 100644 --- a/packages/api/src/middlewares/auth/methods/strategies/token.ts +++ b/packages/api/src/middlewares/auth/methods/strategies/token.ts @@ -3,7 +3,6 @@ import { IAuthRequest } from '@middlewares/auth/types'; import { AuthStrategy } from './type'; import { TokenUtils } from '@src/utils/token/utils'; import { Network } from 'fuels'; -import { TypeUser } from '@src/models'; export class TokenAuthStrategy implements AuthStrategy { async authenticate( diff --git a/packages/api/src/middlewares/auth/methods/strategies/type.ts b/packages/api/src/middlewares/auth/methods/strategies/type.ts index f8ec6593c..399cfbf64 100644 --- a/packages/api/src/middlewares/auth/methods/strategies/type.ts +++ b/packages/api/src/middlewares/auth/methods/strategies/type.ts @@ -1,6 +1,6 @@ import { Request } from 'express'; -import { User, Workspace } from '@src/models'; +import { User, Workspace, DApp } from '@src/models'; import { Network } from 'fuels'; export type IValidatePathParams = { method: string; path: string }; @@ -8,5 +8,5 @@ export type IValidatePathParams = { method: string; path: string }; export interface AuthStrategy { authenticate( req: Request, - ): Promise<{ user: User; workspace: Workspace; network: Network }>; + ): Promise<{ user: User; workspace: Workspace; network: Network; dapp?: DApp }>; } diff --git a/packages/api/src/middlewares/auth/types.ts b/packages/api/src/middlewares/auth/types.ts index 6c82cddea..71f169e00 100644 --- a/packages/api/src/middlewares/auth/types.ts +++ b/packages/api/src/middlewares/auth/types.ts @@ -5,7 +5,7 @@ import { ParsedQs } from 'qs'; import { Workspace } from '@src/models/Workspace'; import UserToken from '@models/UserToken'; -import { User } from '@models/index'; +import { User, DApp } from '@models/index'; import { Network } from 'fuels'; export interface AuthValidatedRequest @@ -19,6 +19,7 @@ export interface AuthValidatedRequest userToken?: UserToken; workspace?: Workspace; network?: Network; + dapp?: DApp; } export interface UnloggedRequest extends Request { diff --git a/packages/api/src/middlewares/handleErrors.ts b/packages/api/src/middlewares/handleErrors.ts index 34fbae2e7..77452af6c 100644 --- a/packages/api/src/middlewares/handleErrors.ts +++ b/packages/api/src/middlewares/handleErrors.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import Express from 'express'; +import { logger } from '@src/config/logger'; import { ExpressJoiError } from 'express-joi-validation'; import GeneralError from '@utils/error/GeneralError'; @@ -16,7 +17,7 @@ const handleErrors = ( next: Express.NextFunction, ) => { process.env.API_ENVIRONMENT === 'production' && - console.log('[REQUEST_ERROR]', err); + logger.error({ error: err }, '[REQUEST_ERROR]'); /** * Error generated by our API */ diff --git a/packages/api/src/middlewares/meld/index.ts b/packages/api/src/middlewares/meld/index.ts new file mode 100644 index 000000000..653159192 --- /dev/null +++ b/packages/api/src/middlewares/meld/index.ts @@ -0,0 +1,104 @@ +import { + ErrorTypes, + Unauthorized, + UnauthorizedErrorTitles, +} from '@src/utils/error'; +import crypto from 'crypto'; +import { NextFunction, Response } from 'express'; +import { IAuthRequest } from '../auth/types'; +import { logger } from '@src/config/logger'; + +/** + * Middleware to verify Meld webhook signatures + * + * Meld signs webhook events using HMAC-SHA256 with the secret defined in the webhook profile. + * The signature is constructed using: base64url(HMACSHA256(..)) + * + * Headers expected: + * - Meld-Signature: The signature to verify against + * - Meld-Signature-Timestamp: The timestamp of the event + */ +export function MeldAuthMiddleware( + req: IAuthRequest, + _res: Response, + next: NextFunction, +) { + try { + const MELD_PRODUCTION_WEBHOOK_SECRET = + process.env.MELD_PRODUCTION_WEBHOOK_SECRET; + const MELD_SANDBOX_WEBHOOK_SECRET = process.env.MELD_SANDBOX_WEBHOOK_SECRET; + + if (!MELD_PRODUCTION_WEBHOOK_SECRET || !MELD_SANDBOX_WEBHOOK_SECRET) { + throw new Unauthorized({ + title: UnauthorizedErrorTitles.MISSING_CREDENTIALS, + detail: + 'MELD_PRODUCTION_WEBHOOK_SECRET or MELD_SANDBOX_WEBHOOK_SECRET environment variable is not set', + type: ErrorTypes.Unauthorized, + }); + } + + const signature = req.headers['meld-signature'] as string; + const timestamp = req.headers['meld-signature-timestamp'] as string; + + if (!signature || !timestamp) { + throw new Unauthorized({ + title: UnauthorizedErrorTitles.MISSING_CREDENTIALS, + detail: 'Meld-Signature and Meld-Signature-Timestamp headers are required', + type: ErrorTypes.Unauthorized, + }); + } + + // Get the raw body as string + const rawBody = JSON.stringify(req.body); + + // Construct the full URL + const protocol = 'https'; + const host = req.get('host'); + + const url = `${protocol}://${host}${req.originalUrl}`; + + // Create the string to sign: TIMESTAMP.URL.BODY + const stringToSign = `${timestamp}.${url}.${rawBody}`; + + // Create HMAC signature + const expectedProductionSignature = crypto + .createHmac('sha256', MELD_PRODUCTION_WEBHOOK_SECRET) + .update(stringToSign) + .digest('base64url') + .replace(/=/g, ''); // Base64 URL encoded without padding + const expectedSandboxSignature = crypto + .createHmac('sha256', MELD_SANDBOX_WEBHOOK_SECRET) + .update(stringToSign) + .digest('base64url') + .replace(/=/g, ''); // Base64 URL encoded without padding + const sanitizedReceived = signature.replace(/=+$/, ''); + + // Compare signatures using timing-safe comparison + if ( + sanitizedReceived !== expectedProductionSignature && + sanitizedReceived !== expectedSandboxSignature + ) { + logger.error( + { + expectedProductionSignature: expectedProductionSignature, + expectedSandboxSignature: expectedSandboxSignature, + received: sanitizedReceived, + stringToSign, + url, + timestamp, + }, + '[MELD] Webhook signature verification failed', + ); + + throw new Unauthorized({ + title: UnauthorizedErrorTitles.INVALID_SIGNATURE, + detail: 'Invalid Meld webhook signature', + type: ErrorTypes.Unauthorized, + }); + } + + next(); + } catch (error) { + next(error); + } +} diff --git a/packages/api/src/middlewares/permissions/predicate.ts b/packages/api/src/middlewares/permissions/predicate.ts index 706465cb9..d496b5d54 100644 --- a/packages/api/src/middlewares/permissions/predicate.ts +++ b/packages/api/src/middlewares/permissions/predicate.ts @@ -30,7 +30,8 @@ const hasPermission = ( : false; const isSigner = permissions.includes(PermissionRoles.SIGNER) - ? JSON.parse(predicate.configurable).SIGNERS.includes(user.address) + ? JSON.parse(predicate.configurable).SIGNERS?.includes(user.address) ?? + JSON.parse(predicate.configurable).SIGNER === user.address : false; return isOwner || isSigner; diff --git a/packages/api/src/middlewares/permissions/transaction.ts b/packages/api/src/middlewares/permissions/transaction.ts index 70ca5c602..ec53d22ac 100644 --- a/packages/api/src/middlewares/permissions/transaction.ts +++ b/packages/api/src/middlewares/permissions/transaction.ts @@ -7,6 +7,7 @@ import { UnauthorizedErrorTitles, } from '@src/utils/error'; import { Transaction } from '@src/models'; +import { logger } from '@src/config/logger'; export interface ITransactionPermissionMiddlewareOptions { transactionSelector: (req: Request) => string; @@ -18,15 +19,16 @@ export const transactionPermissionMiddleware = ( return async (req: Request, _: Response, next: NextFunction) => { try { const transactionHash = options.transactionSelector(req); - if (!transactionHash) return next(); const { user }: IAuthRequest = req; const transaction = await Transaction.createQueryBuilder('t') .select('t.resume') - .where('t.hash IN (:...hashes)', { - hashes: [transactionHash, transactionHash.slice(2)], + .where('t.hash = :hash', { + hash: transactionHash.startsWith(`0x`) + ? transactionHash.slice(2) + : transactionHash, }) .getOne(); @@ -37,7 +39,6 @@ export const transactionPermissionMiddleware = ( detail: `Transaction with hash ${transactionHash} not found`, }); } - if ( transaction.resume.witnesses.every( witness => witness.account !== user.address, @@ -52,6 +53,7 @@ export const transactionPermissionMiddleware = ( return next(); } catch (error) { + logger.error({ error }, '[TRANSACTION_PERMISSION_MIDDLEWARE]'); return next(error); } }; diff --git a/packages/api/src/models/RampTransactions.ts b/packages/api/src/models/RampTransactions.ts new file mode 100644 index 000000000..267adacef --- /dev/null +++ b/packages/api/src/models/RampTransactions.ts @@ -0,0 +1,55 @@ +import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; + +import { IMeldProviderData } from '@src/modules/meld/types'; +import { Transaction } from '.'; +import { Base } from './Base'; +import { User } from './User'; + +export enum RampTransactionProvider { + MELD = 'MELD', +} + +export type ProviderData = IMeldProviderData; + +@Entity('ramp_transactions') +export class RampTransaction extends Base { + @Column({ name: 'provider', type: 'varchar' }) + provider: RampTransactionProvider; + + @Column({ + type: 'jsonb', + name: 'provider_data', + }) + providerData: ProviderData; + + @JoinColumn({ name: 'user_id' }) + @ManyToOne(() => User) + user: User; + + @OneToOne(() => Transaction, transaction => transaction.rampTransaction, { + nullable: true, + }) + @JoinColumn({ name: 'transaction_id' }) + transaction?: Transaction; + + @Column({ name: 'source_currency', type: 'varchar', nullable: true }) + sourceCurrency?: string; + + @Column({ name: 'source_amount', type: 'varchar', nullable: true }) + sourceAmount?: string; + + @Column({ name: 'destination_currency', type: 'varchar', nullable: true }) + destinationCurrency?: string; + + @Column({ name: 'destination_amount', type: 'varchar', nullable: true }) + destinationAmount?: string; + + @Column({ name: 'payment_method', type: 'varchar', nullable: true }) + paymentMethod?: string; + + @Column({ name: 'user_wallet_address', type: 'varchar', nullable: true }) + userWalletAddress?: string; + + @Column({ name: 'is_sandbox', type: 'boolean', default: false }) + isSandbox: boolean; +} diff --git a/packages/api/src/models/RecoverCode.ts b/packages/api/src/models/RecoverCode.ts index cda3dcbe9..bb4373cc6 100644 --- a/packages/api/src/models/RecoverCode.ts +++ b/packages/api/src/models/RecoverCode.ts @@ -43,7 +43,7 @@ class RecoverCode extends Base { validAt: Date; @Column({ name: 'metadata', type: 'jsonb' }) - metadata: { [key: string]: string | number | boolean }; + metadata: { [key: string]: string | number | boolean | Record }; @Column() used: boolean; diff --git a/packages/api/src/models/Transaction.ts b/packages/api/src/models/Transaction.ts index 079185c4d..0eacc15e1 100644 --- a/packages/api/src/models/Transaction.ts +++ b/packages/api/src/models/Transaction.ts @@ -5,28 +5,44 @@ import { TransactionType, } from 'bakosafe'; import { - TransactionRequest, TransactionType as FuelTransactionType, hexlify, Network, + TransactionRequest, } from 'fuels'; -import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; +import { logger } from '@src/config/logger'; import { User } from '@models/User'; -import { Base } from './Base'; -import { Predicate } from './Predicate'; +import { networks } from '@src/constants/networks'; import { ITransactionResponse } from '@src/modules/transaction/types'; import { AssetFormat, formatAssetFromOperations, + formatAssetFromRampTransaction, formatAssets, parseAmount, } from '@src/utils/formatAssets'; -import { networks } from '@src/constants/networks'; +import { Base } from './Base'; +import { Predicate } from './Predicate'; +import { RampTransaction } from './RampTransactions'; const { FUEL_PROVIDER, FUEL_PROVIDER_CHAIN_ID } = process.env; +export enum TransactionTypeBridge { + BRIDGE = 'BRIDGE', +} + +export enum TransactionTypeWithRamp { + ON_RAMP_DEPOSIT = 'ON_RAMP_DEPOSIT', + OFF_RAMP_WITHDRAW = 'OFF_RAMP_WITHDRAW', +} + +export enum TransactionStatusWithRamp { + PENDING_PROVIDER = 'pending_provider', +} + export { TransactionStatus, TransactionType }; @Entity('transactions') @@ -43,7 +59,7 @@ class Transaction extends Base { enum: TransactionType, default: TransactionType.TRANSACTION_SCRIPT, }) - type: TransactionType; + type: TransactionType | TransactionTypeWithRamp | TransactionTypeBridge; @Column({ type: 'jsonb', @@ -56,7 +72,7 @@ class Transaction extends Base { enum: TransactionStatus, default: TransactionStatus.AWAIT_REQUIREMENTS, }) - status: TransactionStatus; + status: TransactionStatus | TransactionStatusWithRamp; @Column({ type: 'jsonb', name: 'summary', @@ -96,6 +112,11 @@ class Transaction extends Base { @ManyToOne(() => Predicate) predicate: Predicate; + @OneToOne(() => RampTransaction, rampTransaction => rampTransaction.transaction, { + nullable: true, + }) + rampTransaction?: RampTransaction; + static getTypeFromTransactionRequest(transactionRequest: TransactionRequest) { const { type } = transactionRequest; const transactionType = { @@ -112,8 +133,16 @@ class Transaction extends Base { static formatTransactionResponse(transaction: Transaction): ITransactionResponse { let assets: AssetFormat[] = []; + const RAMP_OPERATIONS: string[] = [ + TransactionTypeWithRamp.ON_RAMP_DEPOSIT, + TransactionTypeWithRamp.OFF_RAMP_WITHDRAW, + ]; + + const isOnOffRamp = RAMP_OPERATIONS.includes(transaction.type); - if (transaction.summary?.operations && transaction?.predicate) { + if (isOnOffRamp) { + assets = formatAssetFromRampTransaction(transaction); + } else if (transaction.summary?.operations && transaction?.predicate) { assets = formatAssetFromOperations( transaction.summary.operations, transaction.predicate.predicateAddress, @@ -124,6 +153,18 @@ class Transaction extends Base { const result = Object.assign(transaction, { assets, + rampTransaction: transaction.rampTransaction + ? { + ...transaction.rampTransaction, + fiatAmountInUsd: + transaction.rampTransaction?.providerData?.transactionData + ?.fiatAmountInUsd, + providerTransaction: + transaction.rampTransaction?.providerData?.transactionData + ?.serviceProvider, + providerData: undefined, // Avoid sending providerData directly + } + : undefined, summary: transaction.summary ? { ...transaction.summary, @@ -142,27 +183,31 @@ class Transaction extends Base { } getWitnesses() { - const witnesses = this.resume.witnesses - .filter(w => !!w.signature) - .map(w => w.signature); + try { + const witnesses = this.resume.witnesses + .filter(w => !!w.signature) + .map(w => w.signature); - const { witnesses: txWitnesses } = this.txData; + const { witnesses: txWitnesses } = this.txData; - if ('bytecodeWitnessIndex' in this.txData) { - const { bytecodeWitnessIndex } = this.txData; - const bytecode = txWitnesses[bytecodeWitnessIndex]; + if ('bytecodeWitnessIndex' in this.txData) { + const { bytecodeWitnessIndex } = this.txData; + const bytecode = txWitnesses[bytecodeWitnessIndex]; - bytecode && witnesses.splice(bytecodeWitnessIndex, 0, hexlify(bytecode)); - } + bytecode && witnesses.splice(bytecodeWitnessIndex, 0, hexlify(bytecode)); + } - if ('witnessIndex' in this.txData) { - const { witnessIndex } = this.txData; - const bytecode = txWitnesses[witnessIndex]; + if ('witnessIndex' in this.txData) { + const { witnessIndex } = this.txData; + const bytecode = txWitnesses[witnessIndex]; - bytecode && witnesses.splice(witnessIndex, 0, hexlify(bytecode)); - } + bytecode && witnesses.splice(witnessIndex, 0, hexlify(bytecode)); + } - return witnesses; + return witnesses; + } catch (e) { + logger.error({ error: e }, '[GET_WITNESSES]'); + } } } diff --git a/packages/api/src/models/User.ts b/packages/api/src/models/User.ts index 92df4b82b..06b1c13ae 100644 --- a/packages/api/src/models/User.ts +++ b/packages/api/src/models/User.ts @@ -1,6 +1,7 @@ import { Column, Entity } from 'typeorm'; import { Base } from './Base'; +import { TypeUser } from 'bakosafe'; const { FUEL_PROVIDER } = process.env; @@ -11,12 +12,6 @@ export type WebAuthn = { hardware: string; }; -export enum TypeUser { - FUEL = 'FUEL', - WEB_AUTHN = 'WEB_AUTHN', - EVM = 'EVM', -} - export type UserSettings = { inactivesPredicates: string[]; }; @@ -56,6 +51,7 @@ class User extends Base { webauthn: WebAuthn; @Column({ + type: 'varchar', default: TypeUser.FUEL, }) type: TypeUser; diff --git a/packages/api/src/models/UserToken.ts b/packages/api/src/models/UserToken.ts index 2b4c56670..561ee44d7 100644 --- a/packages/api/src/models/UserToken.ts +++ b/packages/api/src/models/UserToken.ts @@ -8,12 +8,14 @@ import { networks } from '@src/constants/networks'; const { FUEL_PROVIDER, FUEL_PROVIDER_CHAIN_ID } = process.env; +/* eslint-disable @typescript-eslint/no-duplicate-enum-values */ export enum Encoder { FUEL = 'FUEL', - METAMASK = 'FUEL', + METAMASK = 'FUEL', // Intentionally same as FUEL - uses same encoder WEB_AUTHN = 'WEB_AUTHN', EVM = 'EVM', } +/* eslint-enable @typescript-eslint/no-duplicate-enum-values */ export interface IFuelTokenPayload { address: string; diff --git a/packages/api/src/models/Workspace.ts b/packages/api/src/models/Workspace.ts index 25a215232..fec91a1a2 100644 --- a/packages/api/src/models/Workspace.ts +++ b/packages/api/src/models/Workspace.ts @@ -76,12 +76,11 @@ export const defaultPermissions = { }, }; - -export type IUserPermissions = {[key in PermissionRoles]: string[]}; +export type IUserPermissions = { [key in PermissionRoles]: string[] }; export interface IPermissions { [key: string]: IUserPermissions; -}; +} /** * PERMISSIONS TYPING */ diff --git a/packages/api/src/modules/addressBook/controller.ts b/packages/api/src/modules/addressBook/controller.ts index 601d8a5ae..69853b89f 100644 --- a/packages/api/src/modules/addressBook/controller.ts +++ b/packages/api/src/modules/addressBook/controller.ts @@ -6,7 +6,7 @@ import { IPagination } from '@src/utils/pagination'; import { ErrorTypes, error } from '@utils/error'; import { Responses, bindMethods, successful } from '@utils/index'; -import { TypeUser } from '@src/models'; +import { TypeUser } from 'bakosafe'; import { IUserService } from '../user/types'; import { WorkspaceService } from '../workspace/services'; import { AddressBookService } from './services'; diff --git a/packages/api/src/modules/auth/controller.ts b/packages/api/src/modules/auth/controller.ts index 07fe7ebe2..708b1348a 100644 --- a/packages/api/src/modules/auth/controller.ts +++ b/packages/api/src/modules/auth/controller.ts @@ -1,4 +1,5 @@ import { addMinutes } from 'date-fns'; +import { logger } from '@src/config/logger'; import { Address } from 'fuels'; import { RecoverCodeType, User } from '@src/models'; @@ -13,8 +14,50 @@ import { IAuthService, ICreateRecoverCodeRequest, ISignInRequest } from './types import App from '@src/server/app'; import { Request } from 'express'; import { FuelProvider } from '@src/utils'; +import { cacheConfig, CacheMetrics } from '@src/config/cache'; +import { Predicate } from '@src/models'; +import { networksByChainId } from '@src/constants/networks'; + const { FUEL_PROVIDER } = process.env; +// All networks to warmup on login +const WARMUP_NETWORKS = Object.values(networksByChainId); + +/** + * Simple concurrency limiter for warmup requests + */ +function createConcurrencyLimiter(concurrency: number) { + let running = 0; + const queue: (() => void)[] = []; + + const next = () => { + if (running < concurrency && queue.length > 0) { + running++; + const fn = queue.shift(); + fn?.(); + } + }; + + return async (fn: () => Promise): Promise => { + return new Promise((resolve, reject) => { + const run = async () => { + try { + const result = await fn(); + resolve(result); + } catch (err) { + reject(err); + } finally { + running--; + next(); + } + }; + + queue.push(run); + next(); + }); + }; +} + export class AuthController { private authService: IAuthService; @@ -26,7 +69,9 @@ export class AuthController { async signIn(req: ISignInRequest) { try { const { digest, encoder, signature, userAddress, name } = req.body; - const userFilter = userAddress ? { address: new Address(userAddress).toB256() } : { name }; + const userFilter = userAddress + ? { address: new Address(userAddress).toB256() } + : { name }; const { userToken, signin } = await TokenUtils.createAuthToken( signature, @@ -39,6 +84,10 @@ export class AuthController { userToken.accessToken, userToken, ); + + // Note: warmup is triggered earlier in generateSignCode + // so cache should already be ready by the time user signs in + return successful(signin, Responses.Ok); } catch (e) { if (e instanceof GeneralError) throw e; @@ -47,6 +96,125 @@ export class AuthController { } } + /** + * Pre-warm balance cache for user's most recently used predicates + * Runs in background to not block login response + * + * Optimizations: + * - Orders by updatedAt (most recently used first) + * - Limits to maxPredicates (default 20) + * - Skips predicates already in cache + * - Uses global chainId cache + */ + private async warmupUserBalances( + userId: string, + networkUrl?: string, + ): Promise { + if (!userId || !networkUrl) { + logger.info('[WARMUP] Skipped: missing userId or networkUrl'); + return; + } + + try { + const startTime = Date.now(); + const userIdShort = userId.slice(0, 8); + + // Get chainId from global cache (avoids extra RPC call) + const chainId = await FuelProvider.getChainId(networkUrl); + + // Get user's predicates (members + personal account) ordered by most recently used + const predicates = await Predicate.createQueryBuilder('predicate') + .leftJoin('predicate.members', 'member') + .leftJoin('predicate.owner', 'owner') + .where( + 'member.id = :userId OR (owner.id = :userId AND predicate.root = true)', + { userId }, + ) + .select(['predicate.predicateAddress']) + .orderBy('predicate.updatedAt', 'DESC') + .limit(cacheConfig.warmup.maxPredicates) + .getMany(); + + if (predicates.length === 0) { + logger.info({ userId }, '[WARMUP] No predicates found for user'); + return; + } + + const addresses = predicates.map(p => p.predicateAddress); + + // Filter out already cached predicates (if skipIfCached is enabled) + let addressesToWarmup = addresses; + if (cacheConfig.warmup.skipIfCached) { + const balanceCache = App.getInstance()._balanceCache; + addressesToWarmup = await balanceCache.filterUncached(addresses, chainId); + + if (addressesToWarmup.length === 0) { + logger.info( + { userId, addressesLength: addresses.length }, + '[WARMUP] All predicates already cached for user', + ); + return; + } + + logger.info( + { + userId, + addressesToWarmupCount: addressesToWarmup.length, + addressesCount: addresses.length, + }, + '[WARMUP] Some predicates need warming for user', + ); + } else { + logger.info( + { userId: userIdShort, addressesCount: addresses.length }, + '[WARMUP] Warming predicates for user', + ); + } + + // Get provider + const provider = await FuelProvider.create(networkUrl); + + // Use rate limiting for concurrent requests + const limit = createConcurrencyLimiter(cacheConfig.warmup.concurrency); + + const results = await Promise.allSettled( + addressesToWarmup.map(address => + limit(async () => { + try { + await provider.getBalances(address); + return { success: true, address }; + } catch (err) { + return { + success: false, + address, + error: err instanceof Error ? err.message : 'Unknown error', + }; + } + }), + ), + ); + + const successCount = results.filter( + r => r.status === 'fulfilled' && r.value.success, + ).length; + + CacheMetrics.warmup(successCount); + + const elapsed = Date.now() - startTime; + logger.info( + { + userId: userIdShort, + successCount, + addressesToWarmupCount: addressesToWarmup.length, + elapsed, + }, + '[WARMUP] User predicates warmed', + ); + } catch (error) { + logger.error({ error: error }, '[WARMUP]'); + } + } + async signOut(req: Request) { try { const token = req?.headers?.authorization; @@ -88,6 +256,21 @@ export class AuthController { }, }); + // Trigger warm-up early for ALL networks (when code is generated, before user signs) + // This way cache is ready when user completes login, regardless of which network they use + if (cacheConfig.warmup.enabled) { + Promise.all( + WARMUP_NETWORKS.map(networkUrl => + this.warmupUserBalances(owner.id, networkUrl).catch(err => + logger.error( + { newtork: networkUrl, error: err }, + `[WARMUP] Failed for network`, + ), + ), + ), + ); + } + return successful(response, Responses.Ok); } catch (e) { return error(e.error, e.statusCode); diff --git a/packages/api/src/modules/auth/services.ts b/packages/api/src/modules/auth/services.ts index 54d683506..c7e0f32c9 100644 --- a/packages/api/src/modules/auth/services.ts +++ b/packages/api/src/modules/auth/services.ts @@ -1,4 +1,5 @@ import UserToken from '@models/UserToken'; +import { logger } from '@src/config/logger'; import { Predicate, User } from '@models/index'; import { ErrorTypes } from '@utils/error/GeneralError'; @@ -68,7 +69,7 @@ export class AuthService implements IAuthService { const { user, workspace, ...token } = tokenResult; - // console.log('[FIND_TOKEN_INFO]: ', { user, workspace, token }); + // logger.info({ data: { user, workspace, token } }, '[FIND_TOKEN_INFO]: '); const QBPredicate = Predicate.createQueryBuilder('p') .innerJoin('p.owner', 'owner') .select(['p.id', 'p.root', 'owner.id']) @@ -123,7 +124,7 @@ export class AuthService implements IAuthService { return removedUsers.map(user => user.token); } catch (e) { - console.log('[CLEAR_EXPIRED_TOKEN_ERROR]', e); + logger.error({ data: e }, '[CLEAR_EXPIRED_TOKEN_ERROR]'); } } } diff --git a/packages/api/src/modules/auth/types.ts b/packages/api/src/modules/auth/types.ts index 386d8c962..10117b5b8 100644 --- a/packages/api/src/modules/auth/types.ts +++ b/packages/api/src/modules/auth/types.ts @@ -3,16 +3,11 @@ import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; import { Workspace } from '@src/models/Workspace'; import { Encoder } from '@models/UserToken'; -import { - IPermissions, - UserSettings, - TypeUser, - User, - WebAuthn, -} from '@models/index'; +import { IPermissions, UserSettings, User, WebAuthn } from '@models/index'; import { AuthValidatedRequest, UnloggedRequest } from '@middlewares/auth/types'; import { Network } from 'fuels'; +import { TypeUser } from 'bakosafe'; export interface ICreateUserTokenPayload { user: User; diff --git a/packages/api/src/modules/bridge/controller.ts b/packages/api/src/modules/bridge/controller.ts new file mode 100644 index 000000000..63733694d --- /dev/null +++ b/packages/api/src/modules/bridge/controller.ts @@ -0,0 +1,88 @@ +import { bindMethods, Responses, successful } from '@src/utils'; +import { error } from '@src/utils/error'; + +import { + IRequestCreateBridgeTransaction, + IRequestCreateSwap, + IRequestDestination, + IRequestLimits, + IRequestQuote, +} from './types'; +import { LayersSwapServiceFactory } from './service'; + +export default class LayersSwapController { + constructor(private _factory: typeof LayersSwapServiceFactory) { + bindMethods(this); + } + + async getDestinations(request: IRequestDestination) { + try { + const { fromNetwork, fromToken } = request.query; + const net = request.network; + + const service = this._factory.fromNetwork(net); + + const destinations = await service.getDestinations({ + fromNetwork, + fromToken, + }); + return successful(destinations, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async getLimits(request: IRequestLimits) { + try { + const query = request.query; + const net = request.network; + + const service = this._factory.fromNetwork(net); + const limits = await service.getLimits(query); + return successful(limits, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async getQuotes(request: IRequestQuote) { + try { + const query = request.query; + const net = request.network; + + const service = this._factory.fromNetwork(net); + const quotes = await service.getQuotes(query); + return successful(quotes, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async createSwap(request: IRequestCreateSwap) { + try { + const net = request.network; + + const service = this._factory.fromNetwork(net); + const swap = await service.createSwap(request.body); + return successful(swap, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async createBridgeTransaction(request: IRequestCreateBridgeTransaction) { + try { + const net = request.network; + + const service = this._factory.fromNetwork(net); + const swap = await service.createBridgeTransaction({ + ...request.body, + network: net, + user: request.user, + }); + return successful(swap, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } +} diff --git a/packages/api/src/modules/bridge/routes.ts b/packages/api/src/modules/bridge/routes.ts new file mode 100644 index 000000000..41d251a20 --- /dev/null +++ b/packages/api/src/modules/bridge/routes.ts @@ -0,0 +1,39 @@ +import { authMiddleware } from '@src/middlewares'; +import { handleResponse } from '@src/utils'; +import { Router } from 'express'; +import { LayersSwapServiceFactory } from './service'; +import LayersSwapController from './controller'; +import { + ValidatorCreateSwapRequest, + ValidatorDestinationParams, + ValidatorLimitsParams, + ValidatorQuotesParams, + ValidatorCreateBridgeTransactionRequest, +} from './validations'; + +const controller = new LayersSwapController(LayersSwapServiceFactory); + +const router = Router(); + +router.use(authMiddleware); + +router.get( + '/destinations', + ValidatorDestinationParams, + handleResponse(controller.getDestinations), +); +router.get('/limits', ValidatorLimitsParams, handleResponse(controller.getLimits)); +router.get('/quote', ValidatorQuotesParams, handleResponse(controller.getQuotes)); +router.post( + '/swap', + ValidatorCreateSwapRequest, + handleResponse(controller.createSwap), +); + +router.post( + '/', + ValidatorCreateBridgeTransactionRequest, + handleResponse(controller.createBridgeTransaction), +); + +export default router; diff --git a/packages/api/src/modules/bridge/service.ts b/packages/api/src/modules/bridge/service.ts new file mode 100644 index 000000000..98cd2081e --- /dev/null +++ b/packages/api/src/modules/bridge/service.ts @@ -0,0 +1,347 @@ +import { + ErrorTypes, + Internal, + Unauthorized, + UnauthorizedErrorTitles, +} from '@src/utils/error'; +import { + ICreateBridgeTransactionPayload, + ICreateSwapApiResponse, + ICreateSwapPayload, + ICreateSwapResponse, + IGetDestinationPayload, + IGetDestinationsApiResponse, + IGetDestinationsResponse, + IGetLimitsApiResponse, + IGetLimitsResponse, + IGetQuotesApiResponse, + IGetQuotesResponse, + IInfoBridgeSwap, + ILayersSwapService, + ISwapResponse, +} from './types'; +import { createLayersSwapApi, LayersSwapEnv } from './utils'; +import { logger } from '@src/config/logger'; +import axios, { AxiosInstance } from 'axios'; +import { + getTransactionSummaryFromRequest, + Network, + transactionRequestify, +} from 'fuels'; +import { networksByChainId } from '@src/constants/networks'; +import { keysToCamel } from '@src/utils/toCamelCase'; +import { ITransaction } from '../meld/types'; +import { + Predicate, + Transaction, + TransactionStatus, + TransactionType, + TransactionTypeBridge, +} from '@src/models'; +import { FuelProvider, generateWitnessesUpdatedAt } from '@src/utils'; +import { ICreateTransactionPayload } from '../transaction/types'; +import { randomUUID } from 'crypto'; +import { WitnessStatus } from 'bakosafe'; +import { TransactionService } from '../transaction/services'; +import { tokensIDS } from '@src/utils/assets-token/addresses'; + +export class LayersSwapServiceFactory { + static create(env: LayersSwapEnv): LayersSwapService { + return new LayersSwapService(env); + } + + static fromNetwork(net: Network): LayersSwapService { + const env: LayersSwapEnv = + networksByChainId['9889'] === net.url || net.chainId === 9889 + ? 'prod' + : 'sandbox'; + return new LayersSwapService(env); + } +} + +export class LayersSwapService implements ILayersSwapService { + private api: AxiosInstance; + private env: LayersSwapEnv; + + constructor(env: LayersSwapEnv) { + this.env = env; + this.api = createLayersSwapApi(env); + } + + private withVersion(path: string): string { + if (this.env === 'sandbox') { + return path.includes('?') + ? `${path}&version=sandbox` + : `${path}?version=sandbox`; + } + return path; + } + + async getDestinations( + payload: IGetDestinationPayload, + ): Promise { + const { fromNetwork, fromToken } = payload; + try { + const { data: response } = await this.api.get( + this.withVersion( + `/destinations?source_network=${fromNetwork}&source_token=${fromToken}&include_swaps=${true}&include_unmatched=${true}`, + ), + ); + + const optimisNet = response.data.find( + network => network.name === 'OPTIMISM_MAINNET', + ); + + return response.data.map(network => ({ + name: network.name, + displayName: network.display_name, + logo: network.logo, + tokens: network.tokens + .filter(token => token.status === 'active') + .map(token => ({ + symbol: token.symbol, + logo: token.logo, + decimals: token.decimals, + })), + })); + } catch (error) { + throw new Internal({ + title: 'Error fetching destinations from LayersSwap API', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + } + } + + async getLimits(payload: ICreateSwapPayload): Promise { + const params = new URLSearchParams({ + source_network: payload.sourceNetwork, + source_token: payload.sourceToken, + destination_network: payload.destinationNetwork, + destination_token: payload.destinationToken, + }); + + try { + const { data } = await this.api.get( + this.withVersion(`/limits?${params.toString()}`), + ); + + return { + minAmountInUsd: data.data.min_amount_in_usd, + minAmount: data.data.min_amount, + maxAmountInUsd: data.data.max_amount_in_usd, + maxAmount: data.data.max_amount, + }; + } catch (error) { + const isAxiosErr = axios.isAxiosError(error); + const detail = + (isAxiosErr ? error.response?.data?.error?.message : null) || + (error instanceof Error ? error.message : 'Unknown error'); + + throw new Internal({ + title: 'Error fetching limits from LayersSwap API', + detail, + type: ErrorTypes.Internal, + }); + } + } + + async getQuotes(payload: ICreateSwapPayload): Promise { + const params = new URLSearchParams({ + source_network: payload.sourceNetwork, + source_token: payload.sourceToken, + destination_network: payload.destinationNetwork, + destination_token: payload.destinationToken, + amount: payload.amount.toString(), + }); + + try { + const { data } = await this.api.get( + this.withVersion(`/quote?${params.toString()}`), + ); + + return keysToCamel(data.data); + } catch (error) { + const isAxiosErr = axios.isAxiosError(error); + const detail = + (isAxiosErr ? error.response?.data?.error?.message : null) || + (error instanceof Error ? error.message : 'Unknown error'); + + throw new Internal({ + title: 'Error fetching quotes from LayersSwap API', + detail, + type: ErrorTypes.Internal, + }); + } + } + + async createSwap(payload: ICreateSwapPayload): Promise { + try { + const payloadToApi = { + destination_address: payload.destinationAddress, + source_network: payload.sourceNetwork, + source_token: payload.sourceToken, + destination_network: payload.destinationNetwork, + destination_token: payload.destinationToken, + amount: payload.amount, + refuel: payload.refuel, + use_deposit_address: payload.useDepositAddress, + use_new_deposit_address: payload.useNewDepositAddress, + reference_id: payload.referenceId, + slippage: payload.slippage, + refund_address: payload.sourceAddress, + }; + + const { data } = await this.api.post( + this.withVersion(`/swaps`), + payloadToApi, + ); + + return keysToCamel(data.data); + } catch (error) { + const isAxiosErr = axios.isAxiosError(error); + const detail = + (isAxiosErr ? error.response?.data?.error?.message : null) || + (error instanceof Error ? error.message : 'Unknown error'); + + const errorTitle = 'Error create swap LayersSwap API'; + const title = detail.includes('Invalid address') + ? `${errorTitle} - Invalid address` + : errorTitle; + + throw new Internal({ + title, + detail, + type: ErrorTypes.Internal, + }); + } + } + + async createBridgeTransaction( + payload: ICreateBridgeTransactionPayload, + ): Promise { + try { + const { swap, txData, name, network, user } = payload; + + const swapData = swap.swap; + + const predicate = await Predicate.findOneOrFail({ + where: { predicateAddress: swap.sourceAddress }, + relations: { members: true, owner: true }, + }); + + const validUser = + user.id === predicate.owner.id || + predicate.members.some(m => m.id === user.id); + + if (!validUser) { + throw new Unauthorized({ + type: ErrorTypes.Unauthorized, + title: UnauthorizedErrorTitles.UNAUTHORIZED_RESOURCE, + detail: 'The provided resource is unauthorized', + }); + } + + const config = JSON.parse(predicate.configurable); + + const txSummary = await getTransactionSummaryFromRequest({ + transactionRequest: transactionRequestify(txData), + provider: await FuelProvider.create(network.url), + }); + + const witnesses = predicate.members.map(member => ({ + account: member.address, + status: WitnessStatus.PENDING, + signature: null, + updatedAt: generateWitnessesUpdatedAt(), + })); + + const depositActions = swapData.depositActions[0]; + const quote = swapData.quote; + const defaultDecimals = 9; + const isAssetToEth = swap.destinationAsset === tokensIDS.ETH; + + const swapInfo: IInfoBridgeSwap = { + id: swapData.swap.id, + createdDate: swapData.swap.createdDate, + sourceNetwork: swapData.swap.sourceNetwork, + sourceAddress: swap.sourceAddress, + sourceToken: { + assetId: swap.sourceAsset, + amount: swapData.swap.requestedAmount, + to: depositActions.toAddress, + decimals: swapData?.swap?.sourceToken?.decimals ?? defaultDecimals, + }, + destinationNetwork: swapData.swap.destinationNetwork, + destinationToken: { + assetId: swap.destinationAsset, + amount: quote.receiveAmount, + to: swapData.swap.destinationAddress, + decimals: isAssetToEth + ? defaultDecimals + : swapData?.swap?.destinationToken?.decimals ?? defaultDecimals, + }, + status: swapData.swap.status, + }; + + const txPayload: ICreateTransactionPayload = { + name: + name ?? + `Bridge ${swapData.swap.sourceNetwork.name} to ${swapData.swap.destinationNetwork.name}`, + predicateAddress: swap.sourceAddress, + hash: txSummary.id.slice(2), + txData, + status: TransactionStatus.AWAIT_REQUIREMENTS, + resume: { + hash: txSummary.id, + status: TransactionStatus.AWAIT_REQUIREMENTS, + witnesses, + requiredSigners: config.SIGNATURES_COUNT ?? 1, + totalSigners: predicate.members.length, + predicate: { + id: predicate.id, + address: predicate.predicateAddress, + }, + id: txSummary.id, + // @ts-expect-error not defined in resume type + bridge: swapInfo, + }, + type: TransactionTypeBridge.BRIDGE, + createdBy: user, + // @ts-expect-error - no summary.type for this transaction + summary: { operations: txSummary.operations }, + network, + predicate, + }; + + const transaction = await Transaction.create(txPayload) + .save() + .then(res => res) + .catch(err => { + throw new Internal({ + title: 'Error creating transaction', + detail: err instanceof Error ? err.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + }); + + return transaction; + } catch (error) { + logger.info({ data: error }, '[LAYERS_SWAP_CREATE_BRIDGE_TX]'); + const isAxiosErr = axios.isAxiosError(error); + const detail = + (isAxiosErr ? error.response?.data?.error?.message : null) || + (error instanceof Error ? error.message : 'Unknown error'); + + if (error instanceof Unauthorized) { + throw error; + } + + throw new Internal({ + title: 'Error create bridge transaction LayersSwap API', + detail, + type: ErrorTypes.Internal, + }); + } + } +} diff --git a/packages/api/src/modules/bridge/types.ts b/packages/api/src/modules/bridge/types.ts new file mode 100644 index 000000000..bd44571e3 --- /dev/null +++ b/packages/api/src/modules/bridge/types.ts @@ -0,0 +1,486 @@ +import { AuthValidatedRequest } from '@src/middlewares/auth/types'; +import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; +import { Transaction, User } from '@src/models'; +import { Network, TransactionRequest } from 'fuels'; + +export interface ErrorResponse { + code: string; + message: string; + metadata: { + additionalProp1: string; + additionalProp2: string; + additionalProp3: string; + }; +} + +export interface TokenLayersSwapApi { + symbol: string; + display_asset: string; + logo: string; + contract: string; + decimals: number; + price_in_usd: number; + precision: number; + listing_date: Date; + source_rank: number; + destination_rank: number; +} + +export interface MetadataDestinationApi { + listing_date: Date; + evm_oracle_contract: string; + evm_multicall_contract: string; + zks_paymaster_contract: string; + watchdog_contract: string; +} + +export interface INetworkLayersSwapApi { + name: string; + display_name: string; + logo: string; + chain_id: string; + node_url: string; + type: 'evm'; + transaction_explorer_template: string; + account_explorer_template: string; + source_rank: 0; + destination_rank: 0; + token: TokenLayersSwapApi; + metadata: MetadataDestinationApi; + deposit_methods: string[]; +} + +export interface TokenLayersSwap { + symbol: string; + displayAsset: string; + logo: string; + contract: string; + decimals: number; + priceInUsd: number; + precision: number; + listingDate: Date; + sourceRank: number; + destinationRank: number; +} + +export interface MetadataDestination { + listingDate: Date; + evmOracleContract: string; + evmMulticallContract: string; + zksPaymasterContract: string; + watchdogContract: string; +} + +export interface INetworkLayersSwap { + name: string; + displayName: string; + logo: string; + chainId: string; + nodeUrl: string; + type: 'evm'; + transactionExplorerTemplate: string; + accountExplorerTemplate: string; + sourceRank: number; + destinationRank: number; + token: TokenLayersSwap; + metadata: MetadataDestination; + depositMethods: string[]; +} + +export interface IGetDestinationPayload { + fromNetwork: string; + fromToken: string; +} + +export interface ICreateSwapPayload { + destinationAddress: string; + sourceNetwork: string; + sourceToken: string; + destinationNetwork: string; + destinationToken: string; + amount?: number; + refuel?: boolean; + useDepositAddress?: boolean; + useNewDepositAddress?: null; + referenceId?: null; + sourceAddress?: null; + slippage?: null; +} + +export interface IGetDestinationsResponse { + name: string; + displayName: string; + logo: string; + tokens: { + symbol: string; + logo: string; + decimals: number; + }[]; +} + +export interface IGetDestinationsApiResponse { + error: ErrorResponse; + data: [ + { + name: string; + display_name: string; + logo: string; + chain_id: string; + node_url: string; + type: 'evm'; + transaction_explorer_template: string; + account_explorer_template: string; + source_rank: number; + destination_rank: number; + token: TokenLayersSwap; + metadata: MetadataDestination; + deposit_methods: string[]; + tokens: [ + { + symbol: string; + display_asset: string; + logo: string; + contract: string; + decimals: number; + price_in_usd: number; + precision: number; + listing_date: Date; + source_rank: number; + destination_rank: number; + status: string; + refuel: { + token: TokenLayersSwap; + network: INetworkLayersSwap; + amount: number; + amount_in_usd: number; + }; + }, + ]; + }, + ]; +} + +export interface ICreateBridgeTransactionPayloadRequest { + txData: TransactionRequest; + swap: IInfoBridgeSwapPayload; + name?: string; +} + +export interface ICreateBridgeTransactionPayload + extends ICreateBridgeTransactionPayloadRequest { + network: Network; + user: User; +} + +export interface ISwapResponse { + id: string; + createdDate: Date; + sourceNetwork: INetworkLayersSwap; + sourceToken: TokenLayersSwap; + sourceExchange: { + name: string; + displayName: string; + logo: string; + metadata: { + oauth: { + authorizeUrl: string; + connectUrl: string; + }; + listingDate: Date; + }; + }; + destinationNetwork: INetworkLayersSwap; + destinationToken: TokenLayersSwap; + destinationExchange: { + name: string; + displayName: string; + logo: string; + metadata: { + oauth: { + authorizeUrl: string; + connectUrl: string; + }; + listingDate: Date; + }; + }; + requestedAmount: number; + destinationAddress: string; + status: string; + failReason: string; + useDepositAddress: boolean; + metadata: { + sequenceNumber: number; + referenceId: string; + exchangeAccount: string; + }; + transactions: [ + { + from: string; + to: string; + timestamp: Date; + transactionHash: string; + confirmations: number; + maxConfirmations: number; + amount: number; + type: string; + status: string; + token: TokenLayersSwap; + network: INetworkLayersSwap; + feeAmount: number; + feeToken: TokenLayersSwap; + }, + ]; +} + +export interface IInfoBridgeSwap { + id: string; + createdDate: Date; + sourceNetwork: INetworkLayersSwap; + sourceAddress: string; + sourceToken: { + assetId: string; + amount: number; + to: string; + decimals: number; + }; + destinationNetwork: INetworkLayersSwap; + destinationToken: { + assetId: string; + amount: number; + to: string; + decimals: number; + }; + status: string; +} + +export interface IInfoBridgeSwapPayload { + swap: ICreateSwapResponse; + sourceAddress: string; + sourceAsset: string; + destinationAsset: string; +} + +export interface ICreateSwapResponse { + quote: { + totalFee: number; + totalFeeInUsd: number; + sourceNetwork: INetworkLayersSwap; + sourceToken: TokenLayersSwap; + destinationNetwork: INetworkLayersSwap; + destinationToken: TokenLayersSwap; + requestedAmount: number; + receiveAmount: number; + feeDiscount: number; + minReceiveAmount: number; + blockchainFee: number; + serviceFee: number; + avgCompletionTime: string; + refuelInSource: number; + slippage: number; + }; + refuel: { + token: TokenLayersSwap; + network: INetworkLayersSwap; + amount: number; + amountInUsd: number; + }; + reward: { + token: TokenLayersSwap; + network: INetworkLayersSwap; + amount: number; + amountInUsd: number; + campaignType: string; + nftContractAddress: string; + }; + swap: ISwapResponse; + depositActions: [ + { + type: string; + toAddress: string; + amount: number; + order: number; + amountInBaseUnits: string; + network: INetworkLayersSwap; + token: TokenLayersSwap; + feeToken: TokenLayersSwap; + callData: string; + }, + ]; +} + +export interface ICreateSwapApiResponse { + error: ErrorResponse; + data: { + quote: { + total_fee: number; + total_fee_in_usd: number; + source_network: INetworkLayersSwapApi; + source_token: TokenLayersSwapApi; + destination_network: INetworkLayersSwapApi; + destination_token: TokenLayersSwapApi; + requested_amount: number; + receive_amount: number; + fee_discount: number; + min_receive_amount: number; + blockchain_fee: number; + service_fee: number; + avg_completion_time: string; + refuel_in_source: number; + slippage: number; + }; + refuel: { + token: TokenLayersSwapApi; + network: INetworkLayersSwapApi; + amount: number; + amount_in_usd: number; + }; + reward: { + token: TokenLayersSwapApi; + network: INetworkLayersSwapApi; + amount: number; + amount_in_usd: number; + campaign_type: string; + nft_contract_address: string; + }; + swap: ISwapResponse; + deposit_actions: [ + { + type: string; + to_address: string; + amount: number; + order: number; + amount_in_base_units: string; + network: INetworkLayersSwapApi; + token: TokenLayersSwapApi; + fee_token: TokenLayersSwapApi; + call_data: string; + }, + ]; + }; +} + +export interface IGetQuotesApiResponse { + error: ErrorResponse; + data: { + quote: { + total_fee: number; + total_fee_in_usd: number; + source_network: INetworkLayersSwapApi; + source_token: TokenLayersSwapApi; + destination_network: INetworkLayersSwapApi; + destination_token: TokenLayersSwapApi; + requested_amount: number; + receive_amount: number; + fee_discount: number; + min_receive_amount: number; + blockchain_fee: number; + service_fee: number; + avg_completion_time: string; + refuel_in_source: number; + slippage: number; + }; + refuel: { + token: TokenLayersSwapApi; + network: INetworkLayersSwapApi; + amount: number; + amount_in_usd: number; + }; + reward: { + token: TokenLayersSwapApi; + network: INetworkLayersSwapApi; + amount: number; + amount_in_usd: number; + campaign_type: string; + nft_contract_address: string; + }; + }; +} + +export interface IGetQuotesResponse { + quote: { + totalFee: number; + totalFeeInUsd: number; + sourceNetwork: INetworkLayersSwap; + sourceToken: TokenLayersSwap; + destinationNetwork: INetworkLayersSwap; + destinationToken: TokenLayersSwap; + requestedAmount: number; + receiveAmount: number; + feeDiscount: number; + minReceiveAmount: number; + blockchainFee: number; + serviceFee: number; + avgCompletionTime: string; + refuelInSource: number; + slippage: number; + }; + refuel: { + token: TokenLayersSwap; + network: INetworkLayersSwap; + amount: number; + amountInUsd: number; + }; + reward: { + token: TokenLayersSwap; + network: INetworkLayersSwap; + amount: number; + amountInUsd: number; + campaignType: string; + nftContractAddress: string; + }; +} + +export interface IGetLimitsResponse { + minAmountInUsd: number; + minAmount: number; + maxAmountInUsd: number; + maxAmount: number; +} + +export interface IGetLimitsApiResponse { + error: ErrorResponse; + data: { + min_amount_in_usd: number; + min_amount: number; + max_amount_in_usd: number; + max_amount: number; + }; +} + +export interface ILayersSwapService { + getDestinations( + payload: IGetDestinationPayload, + ): Promise; + getLimits(payload: ICreateSwapPayload): Promise; + getQuotes(payload: ICreateSwapPayload): Promise; + createSwap(payload: ICreateSwapPayload): Promise; + createBridgeTransaction( + payload: ICreateBridgeTransactionPayload, + ): Promise; +} + +interface IGetDestinationRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Query]: IGetDestinationPayload; +} + +interface IGetLimitsRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Query]: ICreateSwapPayload; +} + +interface IGetQuoteRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Query]: ICreateSwapPayload; +} + +interface ICreateSwapRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Body]: ICreateSwapPayload; +} + +interface ICreateBridgeTransactionRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Body]: ICreateBridgeTransactionPayloadRequest; +} + +export type IRequestDestination = AuthValidatedRequest; +export type IRequestLimits = AuthValidatedRequest; +export type IRequestQuote = AuthValidatedRequest; +export type IRequestCreateSwap = AuthValidatedRequest; +export type IRequestCreateBridgeTransaction = AuthValidatedRequest; diff --git a/packages/api/src/modules/bridge/utils.ts b/packages/api/src/modules/bridge/utils.ts new file mode 100644 index 000000000..bdba2bb04 --- /dev/null +++ b/packages/api/src/modules/bridge/utils.ts @@ -0,0 +1,39 @@ +import axios, { AxiosInstance } from 'axios'; +import { logger } from '@src/config/logger'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const { + LAYERS_SWAP_API_URL, + LAYERS_SWAP_API_KEY_SANDBOX, + LAYERS_SWAP_API_KEY_PROD, +} = process.env; + +if ( + !LAYERS_SWAP_API_URL || + !LAYERS_SWAP_API_KEY_SANDBOX || + !LAYERS_SWAP_API_KEY_PROD +) { + logger.warn( + 'LAYERS_SWAP_API_URL, LAYERS_SWAP_API_KEY_SANDBOX e LAYERS_SWAP_API_KEY_PROD devem estar definidos no .env', + ); +} + +export type LayersSwapEnv = 'sandbox' | 'prod'; + +export function createLayersSwapApi(env: LayersSwapEnv) { + const apiKey = + env === 'sandbox' ? LAYERS_SWAP_API_KEY_SANDBOX : LAYERS_SWAP_API_KEY_PROD; + + const instance = axios.create({ + baseURL: LAYERS_SWAP_API_URL, + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + 'X-LS-APIKEY': apiKey, + }, + }); + + return instance; +} diff --git a/packages/api/src/modules/bridge/validations.ts b/packages/api/src/modules/bridge/validations.ts new file mode 100644 index 000000000..ea506e717 --- /dev/null +++ b/packages/api/src/modules/bridge/validations.ts @@ -0,0 +1,55 @@ +import { validator } from '@src/utils'; +import Joi from 'joi'; + +export const ValidatorDestinationParams = validator.query( + Joi.object({ + fromNetwork: Joi.string().required(), + fromToken: Joi.string().required(), + }), +); + +export const ValidatorLimitsParams = validator.query( + Joi.object({ + destinationAddress: Joi.string().optional(), + sourceNetwork: Joi.string().required(), + sourceToken: Joi.string().required(), + destinationNetwork: Joi.string().required(), + destinationToken: Joi.string().required(), + }), +); + +export const ValidatorQuotesParams = validator.query( + Joi.object({ + destinationAddress: Joi.string().optional(), + sourceNetwork: Joi.string().required(), + sourceToken: Joi.string().required(), + destinationNetwork: Joi.string().required(), + destinationToken: Joi.string().required(), + amount: Joi.string().required(), + }), +); + +export const ValidatorCreateSwapRequest = validator.body( + Joi.object({ + destinationAddress: Joi.string().required(), + sourceNetwork: Joi.string().required(), + sourceToken: Joi.string().required(), + destinationNetwork: Joi.string().required(), + destinationToken: Joi.string().required(), + amount: Joi.number().required(), + refuel: Joi.boolean().required(), + useDepositAddress: Joi.boolean().required(), + useNewDepositAddress: Joi.string().optional().allow(null), + referenceId: Joi.string().optional().allow(null), + sourceAddress: Joi.string().optional().allow(null), + slippage: Joi.string().optional().allow(null), + }), +); + +export const ValidatorCreateBridgeTransactionRequest = validator.body( + Joi.object({ + txData: Joi.object().required(), + swap: Joi.object().required(), + name: Joi.string().optional().allow(null), + }), +); diff --git a/packages/api/src/modules/dApps/controller.ts b/packages/api/src/modules/dApps/controller.ts index 730b07a48..e99231e39 100644 --- a/packages/api/src/modules/dApps/controller.ts +++ b/packages/api/src/modules/dApps/controller.ts @@ -1,6 +1,7 @@ import { TransactionStatus } from 'bakosafe'; import { addMinutes } from 'date-fns'; +// eslint-disable-next-line prettier/prettier import { type DApp, Predicate, RecoverCodeType, User } from '@src/models'; import { SocketClient } from '@src/socket/client'; @@ -12,6 +13,7 @@ import { RecoverCodeService } from '../recoverCode/services'; import { TransactionService } from '../transaction/services'; import { DAppsService } from './service'; import type { + IChangeAccountRequest, IChangeNetworkRequest, ICreateRecoverCodeRequest, ICreateRequest, @@ -21,8 +23,9 @@ import type { import type { ITransactionResponse } from '../transaction/types'; import App from '@src/server/app'; import { SocketEvents, SocketUsernames } from "@src/socket/types"; +import { logger } from '@src/config/logger'; -const { API_URL, FUEL_PROVIDER } = process.env; +const { API_URL } = process.env; const PREFIX = 'dapp'; export class DappController { private _dappService: IDAppsService; @@ -32,6 +35,30 @@ export class DappController { bindMethods(this); } + async changeAccount({ params, headers }: IChangeAccountRequest) { + try { + const { sessionId, vault } = params; + const { origin } = headers; + + const isAddress = vault.startsWith('0x'); + const predicate = await Predicate.findOne({ where: (isAddress) ? { predicateAddress: vault } : { id: vault } }); + if (!predicate) { + throw new NotFound({ + type: ErrorTypes.NotFound, + title: 'Predicate not found', + detail: 'Predicate not found', + }); + } + const dapp = await new DAppsService().findBySessionID(sessionId, origin); + dapp.currentVault = predicate; + await dapp.save(); + return successful(dapp.currentVault, Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } + } + + async connect({ body }: ICreateRequest) { try { const { vaultId, sessionId, name, origin, userAddress, request_id } = body; @@ -39,6 +66,9 @@ export class DappController { let dapp = await new DAppsService().findBySessionID(sessionId, origin); const user = await User.findOne({ where: { address: userAddress } }); const { network } = await TokenUtils.getTokenByUser(user.id); + + logger.info({ id: dapp?.id, name: dapp?.name }, '[DAPP_CONNECT] found dapp in db') + if (!dapp) { dapp = await new DAppsService().create({ sessionId, @@ -60,19 +90,17 @@ export class DappController { dapp.network = network; await dapp.save(); - const socket = new SocketClient(sessionId, API_URL); - socket.sendMessage({ + const socketClient = new SocketClient(sessionId, API_URL); + socketClient.sendMessage({ sessionId, - to: '[CONNECTOR]', + to: SocketUsernames.CONNECTOR, request_id, - type: '[AUTH_CONFIRMED]', + type: SocketEvents.AUTH_CONFIRMED, data: { connected: true, }, - }).then(() => { - socket.disconnect(); - }) + }); await RedisWriteClient.set(`${PREFIX}${sessionId}`, JSON.stringify(dapp)); return successful(true, Responses.Created); @@ -85,7 +113,7 @@ export class DappController { try { const dappCache = await RedisReadClient.get(`${PREFIX}${params.sessionId}`); const dapp = dappCache ? JSON.parse(dappCache) : null; - if(dapp) { + if (dapp) { return successful( dapp.currentVault.predicateAddress, Responses.Ok, @@ -192,7 +220,7 @@ export class DappController { try { const dappCache = await RedisReadClient.get(`${PREFIX}${params.sessionId}`); const dapp = dappCache ? JSON.parse(dappCache) : null; - if(dapp) { + if (dapp) { return successful( dapp.currentVault.predicateAddress, Responses.Ok, @@ -209,7 +237,7 @@ export class DappController { try { const dappCache = await RedisReadClient.get(`${PREFIX}${params.sessionId}`); const _dapp = dappCache ? JSON.parse(dappCache) : null; - if(_dapp) { + if (_dapp) { return successful( _dapp.network.url, Responses.Ok, @@ -238,7 +266,7 @@ export class DappController { try { const dappCache = await RedisReadClient.get(`${PREFIX}${params.sessionId}`); const dapp = dappCache ? JSON.parse(dappCache) : null; - if(dapp) { + if (dapp) { return successful( dapp.vaults.map(vault => vault.predicateAddress), Responses.Ok, @@ -255,26 +283,26 @@ export class DappController { return error(e.error, e.statusCode); } } - + async state({ params, headers }: IDappRequest) { try { const dappCache = await RedisReadClient.get(`${PREFIX}${params.sessionId}`); const dapp = dappCache ? JSON.parse(dappCache) : null; - if(!dapp) { + if (!dapp) { const _dapp = await this._dappService .findBySessionID(params.sessionId, headers.origin || headers.Origin) .then((data: DApp) => { return data; }); - if(!_dapp) { - return successful(false, Responses.Ok); - } - await RedisWriteClient.set(`${PREFIX}${params.sessionId}`, JSON.stringify(_dapp)); - return successful(true, Responses.Ok); + if (!_dapp) { + return successful(false, Responses.Ok); + } + await RedisWriteClient.set(`${PREFIX}${params.sessionId}`, JSON.stringify(_dapp)); + return successful(true, Responses.Ok); } - + return successful( true, Responses.Ok, @@ -333,22 +361,20 @@ export class DappController { await RedisWriteClient.set(`${PREFIX}${sessionId}`, JSON.stringify(dapp)); const socketClient = new SocketClient(dapp.user.id, API_URL); + socketClient.emit( + SocketEvents.SWITCH_NETWORK, + { + sessionId: dapp.user.id, + to: SocketUsernames.UI, + request_id: undefined, + type: SocketEvents.SWITCH_NETWORK, + data: dapp.network, + }, + ); - const socketData = { - sessionId: dapp.user.id, - to: SocketUsernames.UI, - request_id: undefined, - type: SocketEvents.SWITCH_NETWORK, - data: dapp.network, - } - - socketClient.emit(SocketEvents.SWITCH_NETWORK, socketData).then(() => { - socketClient.disconnect(); - }) - return successful(dapp.network, Responses.Ok); } catch (e) { - console.log(e); + logger.error({ error: e }, '[DAPP_CHANGE_NETWORK]'); return error(e.error, e.statusCode); } } diff --git a/packages/api/src/modules/dApps/routes.ts b/packages/api/src/modules/dApps/routes.ts index a9bba6511..e38864205 100644 --- a/packages/api/src/modules/dApps/routes.ts +++ b/packages/api/src/modules/dApps/routes.ts @@ -19,6 +19,7 @@ const { connect, createConnectorCode, changeNetwork, + changeAccount, } = new DappController(dAppService); router.post('/', authMiddleware, handleResponse(connect)); @@ -29,6 +30,7 @@ router.get( ); router.put('/:sessionId/network', handleResponse(changeNetwork)); +router.put('/:sessionId/:vault', handleResponse(changeAccount)); router.get('/:sessionId/state', handleResponse(state)); router.get('/:sessionId/accounts', handleResponse(accounts)); diff --git a/packages/api/src/modules/dApps/types.ts b/packages/api/src/modules/dApps/types.ts index 9326f12af..fc1de5d30 100644 --- a/packages/api/src/modules/dApps/types.ts +++ b/packages/api/src/modules/dApps/types.ts @@ -88,8 +88,17 @@ interface IDappRequestSchema extends ValidatedRequestSchema { [ContainerTypes.Headers]: { origin?: string; Origin?: string }; } +interface IChangeAccountRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Params]: { + sessionId: string; + vault: string; // predicateAddress or id + }; + [ContainerTypes.Headers]: { origin: string }; +} + export type ICreateRecoverCodeRequest = UnloggedRequest; export type IConfirmTxRequest = AuthValidatedRequest; export type ICreateRequest = AuthValidatedRequest; +export type IChangeAccountRequest = AuthValidatedRequest; export type IDappRequest = AuthValidatedRequest; export type IChangeNetworkRequest = AuthValidatedRequest; diff --git a/packages/api/src/modules/external/routes.ts b/packages/api/src/modules/external/routes.ts index 82e2eae00..2d1d2709f 100644 --- a/packages/api/src/modules/external/routes.ts +++ b/packages/api/src/modules/external/routes.ts @@ -1,18 +1,18 @@ import { Router } from 'express'; -import { handleResponse } from '@src/utils'; import { externAuthMiddleware } from '@src/middlewares'; +import { handleResponse } from '@src/utils'; -import { PredicateService } from '../predicate/services'; +import { AddressBookService } from '../addressBook/services'; import { NotificationService } from '../notification/services'; import { PredicateController } from '../predicate/controller'; +import { PredicateService } from '../predicate/services'; import { QuoteController } from '../quote/controller'; import { QuoteService } from '../quote/services'; +import { TransactionController } from '../transaction/controller'; +import { TransactionService } from '../transaction/services'; import { UserController } from '../user/controller'; import { UserService } from '../user/service'; -import { TransactionService } from '../transaction/services'; -import { TransactionController } from '../transaction/controller'; -import { AddressBookService } from '../addressBook/services'; const router = Router(); @@ -28,7 +28,7 @@ const predicateContoller = new PredicateController( notificationsService, ); const quoteController = new QuoteController(quoteService); -const userController = new UserController(userService); +const userController = new UserController(userService, txService); const txController = new TransactionController( txService, predicateService, diff --git a/packages/api/src/modules/healthCheck/controller.ts b/packages/api/src/modules/healthCheck/controller.ts index 803431e27..9b6a0eec8 100644 --- a/packages/api/src/modules/healthCheck/controller.ts +++ b/packages/api/src/modules/healthCheck/controller.ts @@ -1,5 +1,5 @@ import { bindMethods, Responses, successful } from '@src/utils'; -import { error } from '@src/utils/error'; +import { error, Responses as ErrorResponses } from '@src/utils/error'; import { IHealthCheckService } from './types'; export class HealthCheckController { @@ -14,8 +14,11 @@ export class HealthCheckController { try { const result = await this.healthCheckService.checkDatabase(); return successful(result, Responses.Ok); - } catch (e) { - return error(e.error, e.statusCode); + } catch { + return error( + new Error('Database health check failed'), + ErrorResponses.Internal, + ); } } @@ -23,8 +26,8 @@ export class HealthCheckController { try { const result = await this.healthCheckService.checkRedis(); return successful(result, Responses.Ok); - } catch (e) { - return error(e.error, e.statusCode); + } catch { + return error(new Error('Redis health check failed'), ErrorResponses.Internal); } } } diff --git a/packages/api/src/modules/healthCheck/service.ts b/packages/api/src/modules/healthCheck/service.ts index 1347a5ae5..351572467 100644 --- a/packages/api/src/modules/healthCheck/service.ts +++ b/packages/api/src/modules/healthCheck/service.ts @@ -1,3 +1,4 @@ +import { logger } from '@src/config/logger'; import { RedisReadClient } from '@src/utils/redis/RedisReadClient'; import { RedisWriteClient } from '@src/utils/redis/RedisWriteClient'; import { getDatabaseInstance } from '@src/config/connection'; @@ -12,7 +13,7 @@ export class HealthCheckService implements IHealthCheckService { const dataSource = await getDatabaseInstance(); if (!dataSource || !dataSource.isInitialized) { - console.error('[HEALTH_CHECK_DATABASE] Database not initialized'); + logger.error({}, '[HEALTH_CHECK_DATABASE] Database not initialized'); throw new Error('Database not initialized'); } @@ -20,7 +21,7 @@ export class HealthCheckService implements IHealthCheckService { return { status: 'ok' }; } catch (error) { - console.error('[HEALTH_CHECK_DATABASE]', error); + logger.error({ error }, '[HEALTH_CHECK_DATABASE]'); throw error; } } @@ -34,7 +35,7 @@ export class HealthCheckService implements IHealthCheckService { return { status: 'ok' }; } catch (error) { - console.error('[HEALTH_CHECK_REDIS]', error); + logger.error({ error }, '[HEALTH_CHECK_REDIS]'); throw error; } } diff --git a/packages/api/src/modules/internal/routes.ts b/packages/api/src/modules/internal/routes.ts new file mode 100644 index 000000000..bb0f57752 --- /dev/null +++ b/packages/api/src/modules/internal/routes.ts @@ -0,0 +1,286 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { logger } from '@src/config/logger'; +import App from '@src/server/app'; +import { CacheMetrics } from '@src/config/cache'; + +const internalRouter = Router(); + +// Internal API Key for protected endpoints +const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY; + +/** + * Middleware to protect internal endpoints + * Requires X-API-Key header matching INTERNAL_API_KEY env var + * If INTERNAL_API_KEY is not set, endpoints are unprotected (development mode) + */ +const requireApiKey = (req: Request, res: Response, next: NextFunction) => { + if (!INTERNAL_API_KEY) { + // No API key configured - allow access (development mode) + logger.warn('[INTERNAL] No INTERNAL_API_KEY configured, allowing access'); + return next(); + } + + const apiKey = req.headers['x-api-key']; + + if (apiKey !== INTERNAL_API_KEY) { + return res.status(401).json({ error: 'Unauthorized: Invalid API key' }); + } + + next(); +}; + +/** + * GET /internal/cache/stats + * Returns cache statistics and metrics + */ +internalRouter.get('/cache/stats', requireApiKey, async (_req, res) => { + try { + const balanceCache = App.getInstance()._balanceCache; + const stats = await balanceCache.stats(); + + return res.json({ + success: true, + data: stats, + timestamp: Date.now(), + }); + } catch (error) { + logger.error({ error: error }, '[INTERNAL] cache/stats error:'); + return res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } +}); + +/** + * GET /internal/cache/keys + * Returns cache keys matching a pattern (for debugging) + * Query params: + * - pattern: Redis pattern (default: balance:*) + */ +internalRouter.get('/cache/keys', requireApiKey, async (req, res) => { + try { + const pattern = (req.query.pattern as string) || 'balance:*'; + const balanceCache = App.getInstance()._balanceCache; + const keys = await balanceCache.keys(pattern); + + return res.json({ + success: true, + data: { + pattern, + count: keys.length, + keys: keys.slice(0, 100), // Limit to 100 keys + truncated: keys.length > 100, + }, + timestamp: Date.now(), + }); + } catch (error) { + logger.error({ error: error }, '[INTERNAL] cache/keys error'); + return res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } +}); + +/** + * POST /internal/cache/invalidate + * Invalidate cache with various filters + * Body: + * - predicateAddress: string (optional) - Invalidate specific predicate + * - chainId: number (optional) - Invalidate specific chainId (requires predicateAddress) + * - userId: string (optional) - Invalidate all predicates for a user + * - workspaceId: string (optional) - Invalidate all predicates in a workspace + * - all: boolean (optional) - Invalidate ALL cache (use with caution!) + */ +internalRouter.post('/cache/invalidate', requireApiKey, async (req, res) => { + try { + const { predicateAddress, chainId, userId, workspaceId, all } = req.body; + const balanceCache = App.getInstance()._balanceCache; + + let invalidatedCount = 0; + let message = ''; + + // Option 1: Invalidate ALL (dangerous!) + if (all === true) { + invalidatedCount = await balanceCache.invalidateAll(); + message = `Invalidated all cache (${invalidatedCount} keys)`; + } + // Option 2: Invalidate by predicate address + else if (predicateAddress) { + await balanceCache.invalidate(predicateAddress, chainId); + invalidatedCount = 1; + message = chainId + ? `Invalidated ${predicateAddress} for chainId ${chainId}` + : `Invalidated ${predicateAddress} for all chains`; + } + // Option 3: Invalidate by userId + else if (userId) { + // Import dynamically to avoid circular dependencies + const { Predicate } = await import('@src/models'); + + invalidatedCount = await balanceCache.invalidateByUser(userId, async () => { + const predicates = await Predicate.createQueryBuilder('predicate') + .innerJoin('predicate.members', 'member') + .where('member.id = :userId', { userId }) + .select(['predicate.predicateAddress']) + .getMany(); + return predicates.map(p => p.predicateAddress); + }); + message = `Invalidated ${invalidatedCount} predicates for user ${userId}`; + } + // Option 4: Invalidate by workspaceId + else if (workspaceId) { + const { Predicate } = await import('@src/models'); + + const predicates = await Predicate.find({ + where: { workspace: { id: workspaceId } }, + select: ['predicateAddress'], + }); + + for (const predicate of predicates) { + await balanceCache.invalidate(predicate.predicateAddress); + invalidatedCount++; + } + message = `Invalidated ${invalidatedCount} predicates for workspace ${workspaceId}`; + } else { + return res.status(400).json({ + success: false, + error: + 'Missing required parameter: predicateAddress, userId, workspaceId, or all', + }); + } + + logger.info({ message }, '[INTERNAL] cache/invalidate'); + + return res.json({ + success: true, + data: { + invalidatedCount, + message, + }, + timestamp: Date.now(), + }); + } catch (error) { + logger.error({ error: error }, '[INTERNAL] cache/invalidate error'); + return res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } +}); + +/** + * POST /internal/cache/warmup + * Manually trigger cache warmup for a user + * Body: + * - userId: string (required) + * - networkUrl: string (required) + */ +internalRouter.post('/cache/warmup', requireApiKey, async (req, res) => { + try { + const { userId, networkUrl } = req.body; + + if (!userId || !networkUrl) { + return res.status(400).json({ + success: false, + error: 'Missing required parameters: userId, networkUrl', + }); + } + + // Import dynamically to avoid circular dependencies + const { Predicate } = await import('@src/models'); + const { FuelProvider } = await import('@src/utils'); + + // Get user's predicates using query builder for ManyToMany relation + const predicates = await Predicate.createQueryBuilder('predicate') + .innerJoin('predicate.members', 'member') + .where('member.id = :userId', { userId }) + .select(['predicate.predicateAddress']) + .getMany(); + + if (predicates.length === 0) { + return res.json({ + success: true, + data: { + warmedUp: 0, + message: `No predicates found for user ${userId}`, + }, + timestamp: Date.now(), + }); + } + + // Get provider and warm up cache + const provider = await FuelProvider.create(networkUrl); + let warmedUp = 0; + const errors: string[] = []; + + for (const predicate of predicates) { + try { + await provider.getBalances(predicate.predicateAddress); + warmedUp++; + } catch (err) { + errors.push( + `${predicate.predicateAddress}: ${ + err instanceof Error ? err.message : 'Unknown error' + }`, + ); + } + } + + CacheMetrics.warmup(warmedUp); + + return res.json({ + success: true, + data: { + total: predicates.length, + warmedUp, + errors: errors.length > 0 ? errors : undefined, + message: `Warmed up ${warmedUp}/${predicates.length} predicates`, + }, + timestamp: Date.now(), + }); + } catch (error) { + logger.error({ error: error }, '[INTERNAL] cache/warmup error'); + return res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } +}); + +/** + * POST /internal/cache/metrics/reset + * Reset cache metrics (for testing) + */ +internalRouter.post('/cache/metrics/reset', requireApiKey, async (_req, res) => { + try { + await CacheMetrics.reset(); + + return res.json({ + success: true, + message: 'Cache metrics reset', + timestamp: Date.now(), + }); + } catch (error) { + logger.error({ error: error }, '[INTERNAL] cache/metrics/reset error'); + return res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } +}); + +/** + * GET /internal/health + * Simple health check for internal services + */ +internalRouter.get('/health', (_req, res) => { + return res.json({ + success: true, + status: 'healthy', + timestamp: Date.now(), + }); +}); + +export { internalRouter }; diff --git a/packages/api/src/modules/meld/controller.ts b/packages/api/src/modules/meld/controller.ts new file mode 100644 index 000000000..ec014990c --- /dev/null +++ b/packages/api/src/modules/meld/controller.ts @@ -0,0 +1,221 @@ +import { IAuthRequest } from '@src/middlewares/auth/types'; +import { RampTransactionProvider } from '@src/models/RampTransactions'; +import { bindMethods, Responses, successful } from '@src/utils'; +import { error } from '@src/utils/error'; +import { IRampTransactionService } from '../rampTransactions/types'; +import { IMeldService, IRequestCreateWidgetSession, IRequestQuote } from './types'; +import { FIAT_CURRENCIES, formatAmount, getMeldEthValueByNetwork } from './utils'; + +export default class MeldController { + constructor( + private _service: IMeldService, + private _rampService: IRampTransactionService, + ) { + bindMethods(this); + } + + async getCountries(req: IAuthRequest) { + try { + const network = req.network; + const cryptoCurrency = getMeldEthValueByNetwork(network.chainId); + const countries = await this._service.getCountries( + { + accountFilter: true, + cryptoCurrencies: cryptoCurrency, + }, + network, + ); + return successful(countries, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async getFiatCurrencies(req: IAuthRequest) { + try { + const currencies = await this._service.getFiatCurrencies( + { + accountFilter: true, + fiatCurrencies: FIAT_CURRENCIES.join(','), + }, + req.network, + ); + return successful(currencies, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async getOnRampPurchaseLimits(req: IAuthRequest) { + try { + const network = req.network; + const cryptoCurrency = getMeldEthValueByNetwork(network.chainId); + const limits = await this._service.getOnRampPurchaseLimits( + { + accountFilter: true, + fiatCurrencies: FIAT_CURRENCIES.join(','), + cryptoCurrencies: cryptoCurrency, + }, + network, + ); + return successful(limits, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async getOffRampPurchaseLimits(req: IAuthRequest) { + try { + const network = req.network; + const cryptoCurrency = getMeldEthValueByNetwork(network.chainId); + const limits = await this._service.getOffRampPurchaseLimits( + { + accountFilter: true, + fiatCurrencies: FIAT_CURRENCIES.join(','), + cryptoCurrencies: cryptoCurrency, + }, + network, + ); + return successful(limits, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async getCryptoCurrencies(req: IAuthRequest) { + try { + const network = req.network; + const cryptoCurrency = getMeldEthValueByNetwork(network.chainId); + const currencies = await this._service.getCryptoCurrencies( + { + accountFilter: true, + cryptoCurrencies: cryptoCurrency, + }, + network, + ); + return successful(currencies, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async getPaymentMethods(req: IAuthRequest) { + try { + const network = req.network; + const cryptoCurrency = getMeldEthValueByNetwork(network.chainId); + const methods = await this._service.getPaymentMethods( + { + accountFilter: true, + cryptoCurrencies: cryptoCurrency, + }, + network, + ); + return successful(methods, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async getServiceProviders(req: IAuthRequest) { + try { + const network = req.network; + const cryptoCurrency = getMeldEthValueByNetwork(network.chainId); + const serviceProviders = await this._service.getServiceProviders( + { + accountFilter: true, + cryptoCurrencies: cryptoCurrency, + }, + network, + ); + return successful(serviceProviders, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async getQuotes(request: IRequestQuote) { + try { + const network = request.network; + const meldEthValue = getMeldEthValueByNetwork(network.chainId); + const isOnRamp = FIAT_CURRENCIES.includes(request.body.sourceCurrencyCode); + const quote = await this._service.getQuotes( + { + ...request.body, + walletAddress: undefined, + sourceCurrencyCode: isOnRamp + ? request.body.sourceCurrencyCode + : meldEthValue, + destinationCurrencyCode: isOnRamp + ? meldEthValue + : request.body.destinationCurrencyCode, + }, + network, + ); + return successful(quote, Responses.Ok); + } catch (err) { + return error(err.error, err.statusCode); + } + } + + async createWidgetSession(request: IRequestCreateWidgetSession) { + try { + const network = request.network; + const meldEthValue = getMeldEthValueByNetwork(network.chainId); + const isSandbox = network.chainId === 0; + const externalSessionId = `${request.user.id}-${Date.now()}`; + const isOnRamp = request.body.type === 'BUY'; + const session = await this._service.createWidgetSession( + { + sessionType: request.body.type, + externalSessionId, + sessionData: { + lockFields: [ + 'cryptoCurrency', + 'destinationCurrencyCode', + 'sourceCurrencyCode', + 'walletAddress', + ], + countryCode: request.body.countryCode, + destinationCurrencyCode: isOnRamp + ? meldEthValue + : request.body.destinationCurrencyCode, + serviceProvider: request.body.serviceProvider, + sourceCurrencyCode: isOnRamp + ? request.body.sourceCurrencyCode + : meldEthValue, + paymentMethodType: request.body.paymentMethodType, + sourceAmount: formatAmount( + request.body.sourceAmount, + request.body.sourceCurrencyCode, + ), + walletAddress: isSandbox + ? '0xB30fbe035ec95F6106646f77513546e318c7C2DE' + : request.body.walletAddress, + }, + }, + network, + ); + + const rampTransaction = await this._rampService.create({ + provider: RampTransactionProvider.MELD, + user: request.user, + providerData: { + paymentStatus: 'IDLE', + widgetSessionData: session, + transactionData: null, + }, + sourceCurrency: request.body.sourceCurrencyCode, + sourceAmount: request.body.sourceAmount, + destinationCurrency: request.body.destinationCurrencyCode, + paymentMethod: request.body.paymentMethodType, + destinationAmount: request.body.destinationAmount, + userWalletAddress: request.body.walletAddress, + isSandbox: network.chainId === 0, + }); + + return successful(rampTransaction, Responses.Created); + } catch (err) { + return error(err.error, err.statusCode); + } + } +} diff --git a/packages/api/src/modules/meld/routes.ts b/packages/api/src/modules/meld/routes.ts new file mode 100644 index 000000000..c714170fb --- /dev/null +++ b/packages/api/src/modules/meld/routes.ts @@ -0,0 +1,37 @@ +import { authMiddleware } from '@src/middlewares'; +import { handleResponse } from '@src/utils'; +import { Router } from 'express'; +import RampTransactionsService from '../rampTransactions/service'; +import MeldController from './controller'; +import MeldService from './services'; +import { ValidatorCreateWidgetRequest, ValidatorRequestQuote } from './validations'; + +const meldService = new MeldService(); +const rampService = new RampTransactionsService(); +const controller = new MeldController(meldService, rampService); + +const router = Router(); + +router.use(authMiddleware); + +router.get('/countries', handleResponse(controller.getCountries)); +router.get('/fiat-currencies', handleResponse(controller.getFiatCurrencies)); +router.get('/crypto-currencies', handleResponse(controller.getCryptoCurrencies)); +router.get( + '/buy-purchase-limits', + handleResponse(controller.getOnRampPurchaseLimits), +); +router.get( + '/sell-purchase-limits', + handleResponse(controller.getOffRampPurchaseLimits), +); +router.get('/payment-methods', handleResponse(controller.getPaymentMethods)); +router.get('/providers', handleResponse(controller.getServiceProviders)); +router.post('/quotes', ValidatorRequestQuote, handleResponse(controller.getQuotes)); +router.post( + '/widget', + ValidatorCreateWidgetRequest, + handleResponse(controller.createWidgetSession), +); + +export default router; diff --git a/packages/api/src/modules/meld/services.ts b/packages/api/src/modules/meld/services.ts new file mode 100644 index 000000000..3f1efb04d --- /dev/null +++ b/packages/api/src/modules/meld/services.ts @@ -0,0 +1,228 @@ +import { ErrorTypes, Internal } from '@src/utils/error'; +import { AxiosError } from 'axios'; +import { Network } from 'fuels'; +import { + IBuyCryptoRequest, + ICommonSearchParams, + ICreateWidgetResponse, + IFiatCurrencyResponse, + IMeldService, + IPaymentMethodResponse, + IPurchaseLimitsParams, + IPurchaseLimitsResponse, + IQuoteParams, + IQuoteResponse, + ISearchCountryResponse, + ISearchCurrencyResponse, + ISellCryptoRequest, + IServiceProviderParams, + IServiceProviderResponse, +} from './types'; +import { MeldApiFactory } from './utils'; + +export default class MeldService implements IMeldService { + /** + * @description Returns a list of properties which meet the search criteria. + */ + async getCountries( + params: ICommonSearchParams, + network: Network, + ): Promise { + try { + const meldApi = MeldApiFactory.getMeldApiByNetwork(network).api; + const { data } = await meldApi.get( + '/service-providers/properties/countries', + { + params, + }, + ); + return data; + } catch (error) { + throw new Internal({ + title: 'Error fetching countries from Meld API', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + } + } + + /** + * @description Returns a list of properties which meet the search criteria. + */ + async getFiatCurrencies(params: ICommonSearchParams, network: Network) { + try { + const meldApi = MeldApiFactory.getMeldApiByNetwork(network).api; + const { data } = await meldApi.get( + '/service-providers/properties/fiat-currencies', + { + params, + }, + ); + return data; + } catch (error) { + throw new Internal({ + title: 'Error fetching fiat currencies from Meld API', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + } + } + + async getPaymentMethods(params: ICommonSearchParams, network: Network) { + try { + const meldApi = MeldApiFactory.getMeldApiByNetwork(network).api; + const { data } = await meldApi.get( + '/service-providers/properties/payment-methods', + { + params, + }, + ); + return data; + } catch (error) { + throw new Internal({ + title: 'Error fetching payment methods from Meld API', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + } + } + + /** + * @description Returns a list of limits (minimums and maximums) in terms of fiat currencies tokens for buying crypto. + */ + async getOnRampPurchaseLimits( + params: IPurchaseLimitsParams, + network: Network, + ): Promise { + try { + const meldApi = MeldApiFactory.getMeldApiByNetwork(network).api; + const { data } = await meldApi.get( + '/service-providers/limits/fiat-currency-purchases', + { + params, + }, + ); + return data; + } catch (error) { + throw new Internal({ + title: 'Error fetching purchase limits from Meld API', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + } + } + + /** + * @description Returns a list of limits (minimums and maximums) in terms of fiat currencies tokens for buying crypto. + */ + async getOffRampPurchaseLimits( + params: IPurchaseLimitsParams, + network: Network, + ): Promise { + try { + const meldApi = MeldApiFactory.getMeldApiByNetwork(network).api; + const { data } = await meldApi.get( + '/service-providers/limits/crypto-currency-sells', + { + params, + }, + ); + return data; + } catch (error) { + throw new Internal({ + title: 'Error fetching purchase limits from Meld API', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + } + } + + /** + * @description Returns a list of properties which meet the search criteria. + */ + async getCryptoCurrencies(params: ICommonSearchParams, network: Network) { + try { + const meldApi = MeldApiFactory.getMeldApiByNetwork(network).api; + const { data } = await meldApi.get( + '/service-providers/properties/crypto-currencies', + { + params, + }, + ); + return data; + } catch (error) { + throw new Internal({ + title: 'Error fetching crypto currencies from Meld API', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + } + } + + async getServiceProviders(params: IServiceProviderParams, network: Network) { + try { + const meldApi = MeldApiFactory.getMeldApiByNetwork(network).api; + const { data } = await meldApi.get( + '/service-providers', + { + params, + }, + ); + return data; + } catch (error) { + throw new Internal({ + title: 'Error fetching service providers from Meld API', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + } + } + + /** + * @description Use this endpoint to request the current exchange rate of the selected fiat currency-cryptocurrency pair, and the required fees. Enter a fiat currency as the sourceCurrencyCode to buy crypto and enter a crypto currency in that field to sell crypto. + */ + async getQuotes( + payload: IQuoteParams, + network: Network, + ): Promise { + try { + const meldApi = MeldApiFactory.getMeldApiByNetwork(network); + return await meldApi.getMeldQuotes(payload); + } catch (error) { + throw new Internal({ + title: 'Error fetching quotes from Meld API', + detail: + error instanceof AxiosError + ? error.response?.data?.message + : error instanceof Error + ? error.message + : 'Unknown error', + type: ErrorTypes.Internal, + }); + } + } + + /** + * @description Use this endpoint to create a crypto widget for a session to buy or sell crypto. + */ + async createWidgetSession( + request: IBuyCryptoRequest | ISellCryptoRequest, + network: Network, + ): Promise { + try { + const meldApi = MeldApiFactory.getMeldApiByNetwork(network); + return await meldApi.createMeldWidgetSession(request); + } catch (error) { + throw new Internal({ + title: 'Error creating widget session from Meld API', + detail: + error instanceof AxiosError + ? error.response?.data?.message + : error instanceof Error + ? error.message + : 'Unknown error', + type: ErrorTypes.Internal, + }); + } + } +} diff --git a/packages/api/src/modules/meld/types.ts b/packages/api/src/modules/meld/types.ts new file mode 100644 index 000000000..0cc9e4ba1 --- /dev/null +++ b/packages/api/src/modules/meld/types.ts @@ -0,0 +1,361 @@ +import { AuthValidatedRequest } from '@src/middlewares/auth/types'; +import { User } from '@src/models'; +import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; +import { Network } from 'fuels'; + +export interface IBuyCryptoRequest { + sessionData: { + countryCode: string; + destinationCurrencyCode: string; + institutionId?: string; + lockFields?: string[]; + paymentMethodType?: string; + redirectUrl?: string; + serviceProvider: string; + sourceCurrencyCode: string; + sourceAmount: string; + walletAddress: string; + walletTag?: string; + }; + sessionType: 'BUY'; +} + +export interface ISellCryptoRequest { + customerId?: string; + externalCustomerId?: string; + externalSessionId?: string; + sessionData: { + countryCode: string; + destinationCurrencyCode: string; + lockFields?: string[]; + paymentMethodType?: string; + redirectUrl?: string; + redirectFlow?: boolean; + serviceProvider: string; + sourceAmount: string; + sourceCurrencyCode: string; + walletAddress?: string; + walletTag?: string; + }; + sessionType: 'SELL'; +} + +export interface ICreateWidgetResponse { + id: string; + token: string; + widgetUrl: string; + externalSessionId: string; + externalCustomerId: string; + customerId: string; +} + +export interface IPaymentMethodResponse { + paymentMethod: string; + name: string; + paymentType: string; + logos: { + dark: string; + light: string; + }; +} + +export interface ICommonSearchParams { + serviceProviders?: string; + statuses?: 'LIVE' | 'RECENTLY_ADDED' | 'BUILDING'; + categories?: string; + accountFilter: boolean; + countries?: string; + fiatCurrencies?: string; + cryptoChains?: string; + cryptoCurrencies?: string; + paymentMethodTypes?: string; + includeServiceProviderDetails?: boolean; +} + +export interface ISearchCountryResponse { + countryCode: string; + flagImageUrl: string; + name: string; + regions?: { + name: string; + regionCode: string; + }[]; + serviceProviderDetails?: Record; +} + +export interface IFiatCurrencyResponse { + name: string; + symbolImageUrl: string; + currencyCode: string; +} + +export interface ISearchCurrencyResponse { + currencyCode: string; + symbolImageUrl: string; + name: string; + chainName: string; + chainCode: string; +} + +export interface IServiceProviderParams { + serviceProviders?: string; + cryptoCurrencies?: string; + accountFilter: boolean; +} + +export interface IServiceProviderResponse { + serviceProvider: string; + name: string; + status: string; + categories: string[]; + categoryStatuses: { + CRYPTO_OFFRAMP: string; + CRYPTO_ONRAMP: string; + }; + websiteUrl: string; + customerSupportUrl: string; + logos: { + dark: string; + light: string; + darkShort: string; + lightShort: string; + }; +} + +export interface IPurchaseLimitsParams { + serviceProviders?: string; + statuses?: 'LIVE' | 'RECENTLY_ADDED' | 'BUILDING'; + categories?: string; + accountFilter: boolean; + countries?: string; + fiatCurrencies?: string; + cryptoChains?: string; + cryptoCurrencies?: string; + paymentMethodTypes?: string; + includeDetails?: boolean; +} + +export interface IPurchaseLimitsResponse { + accountDetails?: Record; + currencyCode: string; + defaultAmount: number; + maximumAmount: number; + meldDetails?: Record; + minimumAmount: number; + serviceProviderDetails?: Record; +} + +export type IQuoteParams = { + countryCode: string; + destinationCurrencyCode: string; + customerId?: string; + externalCustomerId?: string; + paymentMethodType?: string; + serviceProviders?: string[]; + sourceAmount: number; + sourceCurrencyCode: string; + subdivision?: string; + walletAddress?: string; +}; + +export interface IQuote { + countryCode: string; + customerScore: number; + destinationAmount: number; + destinationAmountWithoutFees: number; + destinationCurrencyCode: string; + exchangeRate: number; + fiatAmountWithoutFees: number; + institutionName: string; + lowKyc: boolean; + networkFee: number; + partnerFee: number; + paymentMethodType: string; + serviceProvider: string; + sourceAmount: number; + sourceAmountWithoutFees: number; + sourceCurrencyCode: string; + totalFee: number; + transactionFee: number; + transactionType: string; +} + +export interface IQuoteResponse { + message?: string; + error?: unknown; + quotes: IQuote[]; +} + +export interface ITransaction { + accountId?: string; + countryCode?: string; + createdAt: string; + cryptoDetails: { + blockchainTransactionId: string; + chainId: string; + destinationWalletAddress: string; + institution: string; + networkFee: number; + networkFeeInUsd: number; + partnerFee: number; + partnerFeeInUsd: number; + sessionWalletAddress: string; + sourceWalletAddress: string; + totalFee: number; + totalFeeInUsd: number; + transactionFee: number; + transactionFeeInUsd: number; + }; + customer: { + accountId: string; + address: { + addressDetails: { + city: string; + country: string; + firstName: string; + lastName: string; + lineOne: string; + lineTwo: string; + postalCode: string; + region: string; + }; + type: 'BILLING' | 'SHIPPING' | 'RESIDENCE'; + }; + email: string; + externalId: string; + id: string; + name: { + firstName: string; + lastName: string; + }; + phone: string; + serviceProviders: Record; + status: 'ACTIVE' | 'INACTIVE'; + }; + description: string; + destinationAmount: number; + destinationCurrencyCode: string; + externalCustomerId: string; + externalReferenceId: string; + externalSessionId: string; + fiatAmountInUsd: number; + id: string; + isImported: boolean; + isPassthrough: boolean; + key: string; + paymentMethodType?: string; + serviceProvider: string; + sessionId: string; + status: string; + sourceAmount?: number; + sourceCurrencyCode?: string; + transactionType: string; + updatedAt: string; +} + +export type IMeldTransactionResponse = { + transaction: ITransaction; +}; + +export interface IMeldTransactionCryptoWeebhook { + eventType: string; + eventId: string; + timestamp: string; + accountId: string; + version: string; + payload: { + accountId: string; + paymentTransactionId: string; + customerId?: string; + externalCustomerId?: string; + externalSessionId?: string; + paymentTransactionStatus: + | 'PENDING' + | 'SETTLING' + | 'SETTLED' + | 'ERROR' + | 'FAILED' + | 'PENDING_CREATED'; + }; +} + +export interface IMeldWidgetSessionData { + id: string; + token: string; + widgetUrl: string; + externalSessionId: string; + externalCustomerId: string; + customerId: string; +} + +export interface IMeldProviderData { + widgetSessionData: IMeldWidgetSessionData; + transactionData?: ITransaction; + paymentStatus: string; +} + +export interface IMeldPayload { + externalSessionId: string; + sessionId: string; + user: User; + widgetSessionData: IMeldWidgetSessionData; + transactionData: unknown; + paymentStatus?: string; +} + +export interface IMeldService { + getCountries: ( + params: ICommonSearchParams, + network: Network, + ) => Promise; + getFiatCurrencies: ( + params: ICommonSearchParams, + network: Network, + ) => Promise; + getPaymentMethods: ( + params: ICommonSearchParams, + network: Network, + ) => Promise; + getOnRampPurchaseLimits: ( + params: IPurchaseLimitsParams, + network: Network, + ) => Promise; + getOffRampPurchaseLimits: ( + params: IPurchaseLimitsParams, + network: Network, + ) => Promise; + getCryptoCurrencies: ( + params: ICommonSearchParams, + network: Network, + ) => Promise; + getServiceProviders: ( + params: IServiceProviderParams, + network: Network, + ) => Promise; + getQuotes: (params: IQuoteParams, network: Network) => Promise; + createWidgetSession: ( + request: IBuyCryptoRequest | ISellCryptoRequest, + network: Network, + ) => Promise; +} + +interface IGetQuoteRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Body]: IQuoteParams; +} + +interface ICreateWidgetSessionRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Body]: { + type: 'BUY' | 'SELL'; + countryCode: string; + destinationCurrencyCode: string; + serviceProvider: string; + sourceCurrencyCode: string; + sourceAmount: string; + walletAddress?: string; + paymentMethodType?: string; + destinationAmount?: string; + }; +} + +export type IRequestQuote = AuthValidatedRequest; +export type IRequestCreateWidgetSession = AuthValidatedRequest; diff --git a/packages/api/src/modules/meld/utils.ts b/packages/api/src/modules/meld/utils.ts new file mode 100644 index 000000000..89afbcb92 --- /dev/null +++ b/packages/api/src/modules/meld/utils.ts @@ -0,0 +1,132 @@ +import axios, { AxiosInstance } from 'axios'; +import { logger } from '@src/config/logger'; +import dotenv from 'dotenv'; +import { Network } from 'fuels'; +import { + IBuyCryptoRequest, + ICreateWidgetResponse, + IQuoteParams, + IQuoteResponse, + ISellCryptoRequest, + ITransaction, +} from './types'; + +dotenv.config(); + +const { + MELD_PRODUCTION_API_URL, + MELD_PRODUCTION_API_KEY, + MELD_SANDBOX_API_URL, + MELD_SANDBOX_API_KEY, +} = process.env; + +if (!MELD_PRODUCTION_API_URL || !MELD_PRODUCTION_API_KEY) { + logger.warn('MELD_API_URL and MELD_API_KEY must be defined in .env'); +} + +export const FIAT_CURRENCIES = ['BRL', 'USD', 'EUR']; +// export const CRYPTO_CURRENCIES = [isSandbox ? 'ETH' : '']; + +export const formatAmount = (amount: string, currency: string): string => { + if (currency === 'BRL') { + return amount.replace('.', '').replace(',', '.'); + } + return amount; +}; + +// * +// for meld sandbox, use ETH instead of ETH_FUEL +// as they don't support ETH_FUEL in their sandbox environment +// for production, it will use ETH_FUEL as expected +// this is a temporary workaround until meld supports ETH_FUEL in sandbox +// * +export const getMeldEthValueByNetwork = (chainId: number): string => + chainId !== 9889 ? 'ETH' : 'ETH_FUEL'; +// export const meldEthValue = isSandbox ? 'ETH' : 'ETH_FUEL'; + +export const MOCK_DEPOSIT_TX_ID = + '0x192aff0dc8540a69d4fe8652ec4419bf86fb9697f296f2de770ae610caba95d4'; + +// Helper functions for Meld API requests +export class MeldApi { + private _api: AxiosInstance; + + constructor(baseUrl: string, apiKey: string) { + this._api = axios.create({ + baseURL: baseUrl, + headers: { + Authorization: `BASIC ${apiKey}`, + 'Meld-Version': '2025-03-04', + Accept: '*/*', + 'Content-Type': 'application/json', + }, + }); + } + + public getMeldQuotes = async (payload: IQuoteParams): Promise => { + const { data } = await this._api.post( + '/payments/crypto/quote', + payload, + ); + return data; + }; + + public createMeldWidgetSession = async ( + payload: IBuyCryptoRequest | ISellCryptoRequest, + ): Promise => { + const { data } = await this._api.post( + '/crypto/session/widget', + payload, + ); + return data; + }; + + public getMeldTransactions = async (params: { externalSessionIds?: string }) => { + const { data } = await this._api.get<{ + transactions: ITransaction[]; + }>('/payments/transactions', { params }); + return data; + }; + + get api() { + return this._api; + } +} + +export class MeldApiFactory { + static getMeldApiByNetwork = (network: Network) => { + const isSandbox = network.chainId !== 9889; + const baseUrl = isSandbox ? MELD_SANDBOX_API_URL : MELD_PRODUCTION_API_URL; + const apiKey = isSandbox ? MELD_SANDBOX_API_KEY : MELD_PRODUCTION_API_KEY; + + if (!baseUrl || !apiKey) { + throw new Error('MELD_API_URL and MELD_API_KEY must be defined in .env'); + } + + return new MeldApi(baseUrl, apiKey); + }; + + static getMeldEnvironment = (mode: 'production' | 'sandbox') => { + if (mode === 'sandbox') { + if (!MELD_SANDBOX_API_URL || !MELD_SANDBOX_API_KEY) { + throw new Error( + 'MELD_SANDBOX_API_URL and MELD_SANDBOX_API_KEY must be defined in .env', + ); + } + return { + baseUrl: MELD_SANDBOX_API_URL, + apiKey: MELD_SANDBOX_API_KEY, + }; + } else { + if (!MELD_PRODUCTION_API_URL || !MELD_PRODUCTION_API_KEY) { + throw new Error( + 'MELD_PRODUCTION_API_URL and MELD_PRODUCTION_API_KEY must be defined in .env', + ); + } + return { + baseUrl: MELD_PRODUCTION_API_URL, + apiKey: MELD_PRODUCTION_API_KEY, + }; + } + }; +} diff --git a/packages/api/src/modules/meld/validations.ts b/packages/api/src/modules/meld/validations.ts new file mode 100644 index 000000000..200ef2b1e --- /dev/null +++ b/packages/api/src/modules/meld/validations.ts @@ -0,0 +1,34 @@ +import { validator } from '@src/utils'; +import Joi from 'joi'; + +export const ValidatorRequestQuote = validator.body( + Joi.object({ + countryCode: Joi.string().required(), + customerId: Joi.string().optional(), + destinationCurrencyCode: Joi.string().required(), + externalCustomerId: Joi.string().optional(), + paymentMethodType: Joi.string().optional(), + sourceAmount: Joi.number().min(0).required(), + sourceCurrencyCode: Joi.string().required(), + subdivision: Joi.string().optional(), + walletAddress: Joi.string().optional(), + }), +); + +export const ValidatorCreateWidgetRequest = validator.body( + Joi.object({ + type: Joi.string().valid('BUY', 'SELL').required(), + countryCode: Joi.string().required(), + destinationCurrencyCode: Joi.string().required(), + destinationAmount: Joi.string().optional(), + serviceProvider: Joi.string().required(), + sourceAmount: Joi.string().required(), + sourceCurrencyCode: Joi.string().required(), + paymentMethodType: Joi.string().required(), + walletAddress: Joi.string().when('type', { + is: Joi.string().valid('BUY'), + then: Joi.required(), + otherwise: Joi.optional(), + }), + }), +); diff --git a/packages/api/src/modules/notification/routes.ts b/packages/api/src/modules/notification/routes.ts index d2660a41a..126f37489 100644 --- a/packages/api/src/modules/notification/routes.ts +++ b/packages/api/src/modules/notification/routes.ts @@ -1,4 +1,5 @@ import { Router } from 'express'; +import { logger } from '@src/config/logger'; import { authMiddleware } from '@src/middlewares'; import { EmailTemplateType, sendMail } from '@src/utils/EmailSender'; @@ -31,7 +32,7 @@ router.get('/mail', async (_, res) => { await sendMail(EmailTemplateType.TRANSACTION_SIGNED, { to, data }); await sendMail(EmailTemplateType.VAULT_CREATED, { to, data }); } catch (error) { - console.log('🚀 ~ router.get ~ error:', error); + logger.info({ data: error }, '[NOTIFICATION_MAIL_TEST]'); } res.status(200).json(); diff --git a/packages/api/src/modules/notification/services.ts b/packages/api/src/modules/notification/services.ts index 29caa19dc..70e86ed55 100644 --- a/packages/api/src/modules/notification/services.ts +++ b/packages/api/src/modules/notification/services.ts @@ -17,6 +17,7 @@ import { EmailTemplateType, sendMail } from '@src/utils/EmailSender'; import { Network } from 'fuels'; import { SocketEvents, SocketUsernames } from '@src/socket/types'; import { PredicateService } from '../predicate/services'; +import { logger } from '@src/config/logger'; const { API_URL } = process.env; @@ -57,7 +58,7 @@ export class NotificationService implements INotificationService { }); const socketClient = new SocketClient(notification.user_id, API_URL); - socketClient.socket.emit(SocketEvents.NOTIFICATION, { + socketClient.emit(SocketEvents.NOTIFICATION, { sessionId: notification.user_id, to: SocketUsernames.UI, request_id: undefined, @@ -65,7 +66,6 @@ export class NotificationService implements INotificationService { data: {}, }); - socketClient.disconnect(); return notification; } @@ -99,12 +99,15 @@ export class NotificationService implements INotificationService { this._ordination.sort, ); + // Apply default limit to prevent loading entire table when no pagination + const DEFAULT_LIMIT = 100; return hasPagination ? Pagination.create(queryBuilder) .paginate(this._pagination) .then(result => result) .catch(e => Internal.handler(e, 'Error on notification list')) : queryBuilder + .take(DEFAULT_LIMIT) .getMany() .then(notifications => notifications) .catch(e => Internal.handler(e, 'Error on notification list')); @@ -157,18 +160,20 @@ export class NotificationService implements INotificationService { const members = vault.members; - for await (const member of members) { - const socketClient = new SocketClient(member.id, API_URL); - socketClient.socket.emit(SocketEvents.NOTIFICATION, { - sessionId: member.id, - to: SocketUsernames.UI, - request_id: undefined, - type: SocketEvents.VAULT_UPDATE, - data: {}, - }); - - socketClient.disconnect(); - } + // Parallelize socket notifications for all members + await Promise.all( + members.map(member => { + const socketClient = new SocketClient(member.id, API_URL); + socketClient.emit(SocketEvents.NOTIFICATION, { + sessionId: member.id, + to: SocketUsernames.UI, + request_id: undefined, + type: SocketEvents.VAULT_UPDATE, + data: {}, + }); + return Promise.resolve(); + }), + ); } async transactionUpdate(txId: string) { @@ -180,18 +185,21 @@ export class NotificationService implements INotificationService { const members = tx.predicate.members; - for await (const member of members) { - const socketClient = new SocketClient(member.id, API_URL); - socketClient.socket.emit(SocketEvents.NOTIFICATION, { - sessionId: member.id, - to: SocketUsernames.UI, - request_id: undefined, - type: SocketEvents.TRANSACTION_UPDATE, - data: {}, - }); + // Parallelize socket notifications for all members + await Promise.all( + members.map(member => { + const socketClient = new SocketClient(member.id, API_URL); + socketClient.emit(SocketEvents.NOTIFICATION, { + sessionId: member.id, + to: SocketUsernames.UI, + request_id: undefined, + type: SocketEvents.TRANSACTION, + data: {}, + }); - socketClient.disconnect(); - } + return Promise.resolve(); + }), + ); } // select all members of predicate @@ -212,31 +220,48 @@ export class NotificationService implements INotificationService { workspaceId: tx.predicate.workspace.id, }; - for await (const member of members) { - await this.create({ - title: NotificationTitle.TRANSACTION_COMPLETED, - summary, - user_id: member.id, - network, - }); - - if (member.notify) { - await sendMail(EmailTemplateType.TRANSACTION_COMPLETED, { - to: member.email, - data: { summary: { ...summary, name: member?.name ?? '' } }, + // Parallelize notifications for all members + // Each member gets: DB notification + email (if enabled) + socket emit + await Promise.all( + members.map(async member => { + // Create notification and send email/socket in parallel + await Promise.all([ + // Create DB notification + this.create({ + title: NotificationTitle.TRANSACTION_COMPLETED, + summary, + user_id: member.id, + network, + }), + // Send email if user has notifications enabled + member.notify + ? sendMail(EmailTemplateType.TRANSACTION_COMPLETED, { + to: member.email, + data: { summary: { ...summary, name: member?.name ?? '' } }, + }).catch(e => { + logger.error( + { + to: member.email, + memberId: member?.id, + transactionId: summary?.transactionId, + error: e, + }, + '[NOTIFICATION] Failed to send transaction success email', + ); + }) + : Promise.resolve(), + ]); + + // Socket emit (fire and forget) + const socketClient = new SocketClient(member.id, API_URL); + socketClient.emit(SocketEvents.NOTIFICATION, { + sessionId: member.id, + to: SocketUsernames.UI, + request_id: undefined, + type: SocketEvents.TRANSACTION, + data: {}, }); - } - - const socketClient = new SocketClient(member.id, API_URL); - socketClient.socket.emit(SocketEvents.NOTIFICATION, { - sessionId: member.id, - to: SocketUsernames.UI, - request_id: undefined, - type: SocketEvents.TRANSACTION_UPDATE, - data: {}, - }); - - socketClient.disconnect(); - } + }), + ); } } diff --git a/packages/api/src/modules/predicate/controller.ts b/packages/api/src/modules/predicate/controller.ts index 6837a57c3..4a96e2309 100644 --- a/packages/api/src/modules/predicate/controller.ts +++ b/packages/api/src/modules/predicate/controller.ts @@ -1,4 +1,5 @@ import { TransactionStatus } from 'bakosafe'; +import { logger } from '@src/config/logger'; import { Predicate } from '@src/models/Predicate'; import { Workspace } from '@src/models/Workspace'; @@ -6,7 +7,7 @@ import { EmailTemplateType, sendMail } from '@src/utils/EmailSender'; import { NotificationTitle } from '@models/index'; -import { error } from '@utils/error'; +import { error, ErrorTypes, NotFound } from '@utils/error'; import { Responses, bindMethods, @@ -21,19 +22,22 @@ import { INotificationService } from '../notification/types'; import { WorkspaceService } from '../workspace/services'; import { + ICheckPredicateBalancesRequest, ICreatePredicateRequest, IDeletePredicateRequest, IFindByHashRequest, IFindByIdRequest, IFindByNameRequest, + IGetAllocationRequest, IListRequest, IPredicateService, ITooglePredicateRequest, + IUpdatePredicateRequest, PredicateWithHidden, } from './types'; -import { PredicateService } from './services'; import { NotificationService } from '../notification/services'; +import { PredicateService } from './services'; const { FUEL_PROVIDER } = process.env; export class PredicateController { @@ -56,41 +60,104 @@ export class PredicateController { network, workspace, }: ICreatePredicateRequest) { - const predicateService = new PredicateService(); - const predicate = await predicateService.create( - payload, - network, - user, - workspace, + logger.info( + { + name: payload?.name, + predicateAddress: payload?.predicateAddress, + userId: user?.id, + workspaceId: workspace?.id, + }, + '[PREDICATE_CREATE] Starting predicate creation', ); - const notifyDestination = predicate.members.filter( - member => user.id !== member.id, - ); - const notifyContent = { - vaultId: predicate.id, - vaultName: predicate.name, - workspaceId: workspace.id, - }; - for await (const member of notifyDestination) { - await this.notificationService.create({ - title: NotificationTitle.NEW_VAULT_CREATED, - user_id: member.id, - summary: notifyContent, + try { + // If workspace is not provided, use user's single workspace as default + let effectiveWorkspace = workspace; + if (!workspace?.id) { + logger.info( + '[PREDICATE_CREATE] No workspace provided, fetching user single workspace', + ); + effectiveWorkspace = await new WorkspaceService() + .filter({ user: user.id, single: true }) + .list() + .then((response: Workspace[]) => response[0]); + logger.info( + { workspaceId: effectiveWorkspace?.id }, + '[PREDICATE_CREATE] Using single workspace:', + ); + } + + const predicateService = new PredicateService(); + const predicate = await predicateService.create( + payload, network, - }); + user, + effectiveWorkspace, + ); - if (member.notify) { - await sendMail(EmailTemplateType.VAULT_CREATED, { - to: member.email, - data: { summary: { ...notifyContent, name: member?.name ?? '' } }, - }); - } - } + logger.info( + { + predicateId: predicate?.id, + predicateName: predicate?.name, + }, + '[PREDICATE_CREATE] Predicate created successfully', + ); + + const notifyDestination = predicate.members.filter( + member => user.id !== member.id, + ); + const notifyContent = { + vaultId: predicate.id, + vaultName: predicate.name, + workspaceId: effectiveWorkspace.id, + }; + + await Promise.all( + notifyDestination.map(async member => { + try { + await this.notificationService.create({ + title: NotificationTitle.NEW_VAULT_CREATED, + user_id: member.id, + summary: notifyContent, + network, + }); + + if (member.notify && member.email) { + await sendMail(EmailTemplateType.VAULT_CREATED, { + to: member.email, + data: { + summary: { ...notifyContent, name: member?.name ?? '' }, + }, + }); + } + } catch (e) { + logger.error( + { + memberId: member?.id, + to: member.email, + predicateId: predicate.id, + error: e, + }, + '[PREDICATE_CREATE] Failed to process member notification', + ); + } + }), + ); - await new NotificationService().vaultUpdate(predicate.id); + await new NotificationService().vaultUpdate(predicate.id); - return successful(predicate, Responses.Created); + return successful(predicate, Responses.Created); + } catch (e) { + logger.error( + { + message: e?.message || e, + name: e?.name, + stack: e?.stack?.slice(0, 500), + }, + '[PREDICATE_CREATE]', + ); + return error(e.error, e.statusCode); + } } async delete({ params: { id } }: IDeletePredicateRequest) { @@ -115,17 +182,45 @@ export class PredicateController { async findByAddress({ params: { address } }: IFindByHashRequest) { try { + logger.info( + { address }, + '[PREDICATE_FIND_BY_ADDRESS] Looking for predicate:', + ); const predicate = await this.predicateService.findByAddress(address); + if (!predicate) { + logger.info( + { address }, + '[PREDICATE_FIND_BY_ADDRESS] Predicate NOT found for address:', + ); + throw new NotFound({ + type: ErrorTypes.NotFound, + title: 'Predicate not found', + detail: `No predicate found with address ${address}`, + }); + } + + logger.info( + { + predicateId: predicate.id, + predicateName: predicate.name, + membersCount: predicate.members?.length, + }, + '[PREDICATE_FIND_BY_ADDRESS] Predicate found', + ); + return successful(predicate, Responses.Ok); } catch (e) { + logger.error({ error: e?.message || e }, '[PREDICATE_FIND_BY_ADDRESS]'); return error(e.error, e.statusCode); } } async findByName(req: IFindByNameRequest) { + const { ignoreId } = req.query; const { params, workspace } = req; const { name } = params; + try { if (!name || name.length === 0) return successful(false, Responses.Ok); @@ -136,6 +231,10 @@ export class PredicateController { .andWhere('w.id = :workspace', { workspace: workspace.id }) .getOne(); + if (ignoreId && response?.id === ignoreId) { + return successful(false, Responses.Ok); + } + return successful(!!response, Responses.Ok); } catch (e) { return error(e.error, e.statusCode); @@ -205,8 +304,8 @@ export class PredicateController { Responses.Ok, ); } catch (e) { - console.log(`[RESERVED_COINS_ERROR]`, e); - return error(e.error, e.statusCode); + logger.error({ error: e }, '[RESERVED_COINS_ERROR]'); + return error(e.error || e, e.statusCode); } } @@ -334,4 +433,53 @@ export class PredicateController { return error(e.error, e.statusCode); } } + + async update(req: IUpdatePredicateRequest) { + try { + const { predicateId } = req.params; + const { description, name } = req.body; + + const updatedPredicate = await this.predicateService.update(predicateId, { + name, + description, + }); + + return successful(updatedPredicate, Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } + } + + async allocation({ params, user, network }: IGetAllocationRequest) { + const { predicateId } = params; + try { + const allocation = await this.predicateService.allocation({ + user, + predicateId, + network, + assetsMap: (await getAssetsMaps()).assetsMapById, + }); + + return successful(allocation, Responses.Ok); + } catch (e) { + return error(e.error || e, e.statusCode); + } + } + + async checkPredicateBalances({ + params: { predicateId }, + user, + network, + }: ICheckPredicateBalancesRequest) { + try { + await this.predicateService.checkBalances({ + predicateId, + userId: user.id, + network, + }); + return successful(null, Responses.Ok); + } catch (e) { + return error(e.error || e, e.statusCode); + } + } } diff --git a/packages/api/src/modules/predicate/routes.ts b/packages/api/src/modules/predicate/routes.ts index 538566666..da2171d8e 100644 --- a/packages/api/src/modules/predicate/routes.ts +++ b/packages/api/src/modules/predicate/routes.ts @@ -13,6 +13,7 @@ import { validateAddPredicatePayload, validatePredicateIdParams, validateTooglePredicatePayload, + validateUpdatePredicatePayload, } from './validations'; const permissionMiddlewareById = predicatePermissionMiddleware({ @@ -23,9 +24,11 @@ const permissionMiddlewareById = predicatePermissionMiddleware({ }); const permissionMiddlewareByAddress = predicatePermissionMiddleware({ - predicateSelector: req => ({ - predicateAddress: req.params.address, - }), + predicateSelector: req => { + return { + predicateAddress: req.params.address, + }; + }, permissions: [PermissionRoles.OWNER, PermissionRoles.SIGNER], }); @@ -41,12 +44,21 @@ const { hasReservedCoins, checkByAddress, tooglePredicateVisibility, + update, + allocation, + checkPredicateBalances, } = new PredicateController(predicateService, notificationsService); router.use(authMiddleware); router.post('/', validateAddPredicatePayload, handleResponse(create)); router.get('/', handleResponse(list)); +router.put( + '/:predicateId', + validatePredicateIdParams, + validateUpdatePredicatePayload, + handleResponse(update), +); router.get( '/:predicateId', validatePredicateIdParams, @@ -62,7 +74,7 @@ router.get( ); router.get( '/by-address/:address', - permissionMiddlewareByAddress, + // permissionMiddlewareByAddress, handleResponse(findByAddress), ); router.get('/check/by-address/:address', handleResponse(checkByAddress)); @@ -71,4 +83,16 @@ router.put( validateTooglePredicatePayload, handleResponse(tooglePredicateVisibility), ); +router.get( + '/:predicateId/allocation', + validatePredicateIdParams, + handleResponse(allocation), +); +router.get( + '/check-balances/:predicateId', + validatePredicateIdParams, + permissionMiddlewareById, + handleResponse(checkPredicateBalances), +); + export default router; diff --git a/packages/api/src/modules/predicate/services.ts b/packages/api/src/modules/predicate/services.ts index da33a8001..1fcc686e1 100644 --- a/packages/api/src/modules/predicate/services.ts +++ b/packages/api/src/modules/predicate/services.ts @@ -1,21 +1,45 @@ -import { AddressUtils as BakoAddressUtils, DEFAULT_PREDICATE_VERSION, Vault } from 'bakosafe'; +import { + AddressUtils as BakoAddressUtils, + DEFAULT_PREDICATE_VERSION, + getLatestPredicateVersion, + legacyConnectorVersion, + TransactionStatus, + TypeUser, + Vault, + Wallet as WalletType, +} from 'bakosafe'; import { Brackets, MoreThan } from 'typeorm'; import { NotFound } from '@src/utils/error'; import { IPagination, Pagination, PaginationParams } from '@src/utils/pagination'; -import { Predicate, TypeUser, User, Workspace } from '@models/index'; +import { Predicate, User, Workspace } from '@models/index'; import GeneralError, { ErrorTypes } from '@utils/error/GeneralError'; +import { BadRequest } from '@utils/error'; import Internal from '@utils/error/Internal'; -import { IPredicateFilterParams, IPredicatePayload, IPredicateService, } from './types'; -import { IPredicateOrdination, setOrdination } from './ordination'; -import { Network, ZeroBytes32 } from 'fuels'; -import { UserService } from '../user/service'; -import { IconUtils } from '@src/utils/icons'; -import { FuelProvider } from '@src/utils'; import App from '@src/server/app'; +import { calculateReservedCoins, FuelProvider, subCoins } from '@src/utils'; +import { IconUtils } from '@src/utils/icons'; +import { bn, BN, Network, ZeroBytes32 } from 'fuels'; +import { UserService } from '../user/service'; +import { IPredicateOrdination, setOrdination } from './ordination'; +import { + AssetAllocation, + IPredicateAllocation, + IPredicateAllocationParams, + IPredicateFilterParams, + IPredicatePayload, + IPredicateService, +} from './types'; +import { BalanceCache } from '@src/server/storage/balance'; +import { TransactionCache } from '@src/server/storage/transaction'; +import { compareBalances } from '@src/utils/balance'; +import { emitBalanceOutdatedPredicate } from '@src/socket/events'; +import { SocketUsernames, SocketEvents } from '@src/socket/types'; +import { ProviderWithCache } from '@src/utils/ProviderWithCache'; +import { logger } from '@src/config/logger'; export class PredicateService implements IPredicateService { private _ordination: IPredicateOrdination = { @@ -36,8 +60,35 @@ export class PredicateService implements IPredicateService { 'p.owner', 'p.configurable', 'p.root', + 'p.version', ]; + private static async validateUniqueName( + name: string, + workspaceId: string, + type: ErrorTypes, + predicateId?: string, + ): Promise { + const query = Predicate.createQueryBuilder('p') + .where('LOWER(p.name) = LOWER(:name)', { name }) + .andWhere('p.workspace_id = :workspaceId', { workspaceId }) + .andWhere('p.deletedAt IS NULL'); + + if (predicateId) { + query.andWhere('p.id != :predicateId', { predicateId }); + } + + const exists = await query.getOne(); + + if (exists) { + throw new BadRequest({ + type, + title: 'Predicate name already exists', + detail: `A predicate with name "${name}" already exists in this workspace`, + }); + } + } + filter(filter: IPredicateFilterParams) { this._filter = filter; return this; @@ -60,32 +111,46 @@ export class PredicateService implements IPredicateService { workspace: Workspace, ): Promise { try { - const members = []; + await PredicateService.validateUniqueName( + payload.name, + workspace.id, + ErrorTypes.Create, + ); + const userService = new UserService(); - //const workspaceService = new WorkspaceService(); + const config = JSON.parse(payload.configurable); - // create a pending users - const { SIGNERS } = JSON.parse(payload.configurable); - const validUsers = SIGNERS.filter(address => address !== ZeroBytes32); + let members: User[] = []; - for await (const member of validUsers) { - let user = await userService.findByAddress(member); - let type = TypeUser.FUEL; - if (BakoAddressUtils.isEvm(member)) { - type = TypeUser.EVM; - } + // Connector predicate (SIGNER) - owner is the only member + if (config.SIGNER) { + members = [owner]; + } + // Multisig predicate (SIGNERS) - process all signers + else if (config.SIGNERS) { + const validUsers = config.SIGNERS.filter( + (address: string) => address !== ZeroBytes32, + ); - if (!user) { - user = await userService.create({ - address: member, - avatar: IconUtils.user(), - type, - name: member, - provider: network.url, - }); - } + for await (const member of validUsers) { + let user = await userService.findByAddress(member); + let type = TypeUser.FUEL; + if (BakoAddressUtils.isEvm(member)) { + type = TypeUser.EVM; + } + + if (!user) { + user = await userService.create({ + address: member, + avatar: IconUtils.user(), + type, + name: member, + provider: network.url, + }); + } - members.push(user); + members.push(user); + } } // create a predicate @@ -100,7 +165,10 @@ export class PredicateService implements IPredicateService { return await this.findById(predicate.id); // return predicate; } catch (e) { - console.log(e); + logger.error({ error: e }, 'Error on predicate creation'); + + if (e instanceof BadRequest) throw e; + throw new Internal({ type: ErrorTypes.Internal, title: 'Error on predicate creation', @@ -122,13 +190,15 @@ export class PredicateService implements IPredicateService { 'members.avatar', 'members.address', 'members.type', + 'members.notify', + 'members.email', 'owner.id', 'owner.address', 'owner.type', ]) .getOne(); } catch (e) { - console.log(e); + logger.error({ error: e }, 'Error on predicate findById'); if (e instanceof GeneralError) { throw e; } @@ -173,21 +243,38 @@ export class PredicateService implements IPredicateService { async findByAddress(address: string): Promise { try { - return await Predicate.findOne({ - where: { predicateAddress: address }, - relations: ['owner', 'members'], - select: { - owner: { - id: true, - address: true, - }, - members: { - id: true, - address: true, - }, - }, - }); + return await Predicate.createQueryBuilder('p') + .leftJoin('p.owner', 'owner') + .leftJoin('p.members', 'members') + .leftJoin('p.workspace', 'workspace') + .select([ + // Predicate fields (same as predicateFieldsSelection) + 'p.id', + 'p.createdAt', + 'p.deletedAt', + 'p.updatedAt', + 'p.name', + 'p.predicateAddress', + 'p.description', + 'p.configurable', + 'p.root', + 'p.version', + // Relation fields (same as list() method) + 'owner.id', + 'owner.address', + 'owner.avatar', + 'members.id', + 'members.address', + 'members.avatar', + 'workspace.id', + 'workspace.name', + 'workspace.single', + 'workspace.avatar', + ]) + .where('p.predicateAddress = :address', { address }) + .getOne(); } catch (e) { + logger.error({ error: e }, 'Error on predicate findByAddress'); throw new Internal({ type: ErrorTypes.Internal, title: 'Error on predicate findByAddress', @@ -225,6 +312,12 @@ export class PredicateService implements IPredicateService { }); } + if (this._filter.owner) { + queryBuilder.andWhere('owner.id = :owner', { + owner: this._filter.owner, + }); + } + if (this._filter.address) { queryBuilder.andWhere('p.predicateAddress = :predicateAddress', { predicateAddress: this._filter.address, @@ -321,7 +414,9 @@ export class PredicateService implements IPredicateService { if (hasPagination) { return await Pagination.create(queryBuilder).paginate(this._pagination); } else { - const predicates = await queryBuilder.getMany(); + // Apply default limit to prevent loading entire table + const DEFAULT_LIMIT = 100; + const predicates = await queryBuilder.take(DEFAULT_LIMIT).getMany(); return predicates ?? []; } } catch (e) { @@ -337,19 +432,17 @@ export class PredicateService implements IPredicateService { } } - async update(id: string, payload?: IPredicatePayload): Promise { + async update( + id: string, + payload?: Partial, + ): Promise { try { - await Predicate.update( - { id }, - { - ...payload, - updatedAt: new Date(), - }, - ); - - const updatedPredicate = await this.findById(id); + const currentPredicate = await Predicate.findOne({ + where: { id }, + relations: ['workspace'], + }); - if (!updatedPredicate) { + if (!currentPredicate) { throw new NotFound({ type: ErrorTypes.NotFound, title: 'Predicate not found', @@ -357,6 +450,21 @@ export class PredicateService implements IPredicateService { }); } + if (payload?.name && payload.name !== currentPredicate.name) { + await PredicateService.validateUniqueName( + payload.name, + currentPredicate.workspace.id, + ErrorTypes.Update, + currentPredicate.id, + ); + } + + const updatedPredicate = await Predicate.merge(currentPredicate, { + name: payload?.name, + description: payload?.description, + updatedAt: new Date(), + }).save(); + return updatedPredicate; } catch (e) { if (e instanceof NotFound) throw e; @@ -394,6 +502,87 @@ export class PredicateService implements IPredicateService { } } + /** + * Checks and instantiates older predicate versions associated with a user address. + * + * This function retrieves legacy predicate versions linked to a given `address` and `provider`. + * It sorts and filters these versions based on whether they have a balance, instantiates + * relevant versions as `Vault` objects, and identifies "invisible" accounts (those without balance). + * + * ### Behavior: + * - Fetches legacy versions using `legacyConnectorVersion`. + * - Filters versions that have a balance (`hasBalance`) and sorts them by `versionTime` (newest first). + * - Identifies versions without balance and collects their `predicateAddress`. + * - If no versions have a balance: + * - Gets the latest predicate version (`getLatestPredicateVersion`). + * - Creates a default predicate instance using `instancePredicate`. + * - Otherwise: + * - Instantiates all predicates with balance. + * - Detects whether the origin is `EVM` or `SVM` to set the correct configuration. + * + * @async + * @param {string} address - The user's wallet address. + * @param {string} provider - The blockchain provider. + * + * @returns {Promise<{ invisibleAccounts: string[]; accounts: Vault[] }>} + * An object containing: + * - `invisibleAccounts`: List of predicate addresses without balance. + * - `accounts`: List of active `Vault` instances (with balance). + * + * @example + * ```ts + * const { invisibleAccounts, accounts } = await checkOlderPredicateVersions( + * "0x1234abcd...", + * "https://testnet.fuel.network/v1/graphql" + * ); + * + * console.log(invisibleAccounts); // ["0xabc123...", "0xdef456..."] + * console.log(accounts); // [Vault {...}, Vault {...}] + * ``` + */ + async checkOlderPredicateVersions( + address: string, + userType: TypeUser, + provider: string, + ): Promise { + const isEvm = BakoAddressUtils.isEvm(address); + + if (isEvm && userType === TypeUser.EVM) { + const legacyVersions = await legacyConnectorVersion(address, provider); + + const vaults = await Promise.all( + legacyVersions.map(async v => { + const isFromConnector = + v.details.origin === WalletType.EVM || + v.details.origin === WalletType.SVM; + + const config = isFromConnector + ? { SIGNER: address } + : { SIGNERS: [address], SIGNATURES_COUNT: 1 }; + + return this.instancePredicate( + JSON.stringify(config), + provider, + v.version, + ); + }), + ); + + return vaults; + } + + const latest = getLatestPredicateVersion(WalletType.FUEL); + const config = { SIGNERS: [address], SIGNATURES_COUNT: 1 }; + + const vault = await this.instancePredicate( + JSON.stringify(config), + provider, + latest.version, + ); + + return [vault]; + } + async instancePredicate( configurable: string, provider: string, @@ -403,6 +592,7 @@ export class PredicateService implements IPredicateService { const _provider = await FuelProvider.create( provider.replace(/^https?:\/\/[^@]+@/, 'https://'), ); + return new Vault(_provider, conf, version); } @@ -424,4 +614,414 @@ export class PredicateService implements IPredicateService { return await Pagination.create(queryBuilder).paginate(this._pagination); } + + /** + * Extract signers info from vault configurable JSON + */ + private parseVaultSigners( + configurable: string, + ): { members: number; minSigners: number } { + try { + const config = JSON.parse(configurable); + const signers = (config.SIGNERS || []).filter( + (addr: string) => + addr !== + '0x0000000000000000000000000000000000000000000000000000000000000000', + ); + return { + members: signers.length, + minSigners: config.SIGNATURES_COUNT || 1, + }; + } catch { + return { members: 1, minSigners: 1 }; + } + } + + /** + * Build final allocation response from processed data + * @param limit - Maximum number of predicates to return in the response (default: 5) + */ + private buildAllocationResponse( + vaultInfoMap: Map< + string, + { + name: string; + address: string; + members: number; + minSigners: number; + amountInUSD: number; + } + >, + allocationMap: Map, + totalAmountInUSD: number, + limit: number = 5, + ): IPredicateAllocation { + // Calculate percentages and sort + const allocationArray = Array.from(allocationMap.values()) + .filter(allocation => allocation.amountInUSD > 0) + .map(allocation => ({ + ...allocation, + percentage: + totalAmountInUSD > 0 + ? (allocation.amountInUSD / totalAmountInUSD) * 100 + : 0, + })); + + allocationArray.sort((a, b) => b.percentage - a.percentage); + + // Split into top 3 and others + const top3 = allocationArray.slice(0, 3); + const remaining = allocationArray.slice(3); + const finalData = [...top3]; + + if (remaining.length > 0) { + finalData.push({ + assetId: null, + amountInUSD: remaining.reduce((sum, item) => sum + item.amountInUSD, 0), + amount: remaining.reduce((sum, item) => sum.add(item.amount), bn(0)), + percentage: remaining.reduce((sum, item) => sum + item.percentage, 0), + }); + } + + // Convert vaultInfoMap to array, sorted by amountInUSD descending, limited to top N + const predicatesArray = Array.from(vaultInfoMap.entries()) + .map(([id, info]) => ({ + id, + name: info.name, + address: info.address, + members: info.members, + minSigners: info.minSigners, + amountInUSD: info.amountInUSD, + })) + .sort((a, b) => b.amountInUSD - a.amountInUSD) + .slice(0, limit); + + return { + data: finalData, + totalAmountInUSD, + predicates: predicatesArray, + }; + } + + async allocation({ + predicateId, + user, + network, + assetsMap, + limit, + }: IPredicateAllocationParams): Promise { + try { + // ======================================== + // PARALLEL: Fetch vault structures and cache data + // ======================================== + // Fetch ALL predicates for total balance calculation (no limit here) + const structureQuery = Predicate.createQueryBuilder('p') + .leftJoin('p.owner', 'owner') + .leftJoin('p.members', 'members') + .select([ + 'p.id', + 'p.name', + 'p.predicateAddress', + 'p.configurable', + 'p.version', + 'p.updatedAt', + ]) + .distinctOn(['p.id']) + .orderBy('p.id') + .addOrderBy('p.updatedAt', 'DESC'); + + if (predicateId) { + // if a specific predicateId is requested, fetch only that predicate + structureQuery.andWhere('p.id = :predicateId', { predicateId }); + } else { + // otherwise fetch predicates related to the user and exclude inactives + structureQuery.andWhere('(owner.id = :userId OR members.id = :userId)', { + userId: user.id, + }); + + structureQuery.andWhere( + ` + p.predicateAddress NOT IN ( + SELECT jsonb_array_elements_text(u.settings->'inactivesPredicates') + FROM users u + WHERE u.id = :userId + ) + `, + { userId: user.id }, + ); + } + + // Run vault query and cache fetch in parallel + const [vaultStructures, { fuelUnitAssets }, quotes] = await Promise.all([ + structureQuery.getMany(), + import('@src/utils/assets').then(m => m.getAssetsMaps()), + App.getInstance()._quoteCache.getActiveQuotes(), + ]); + + // Build vault info map from structures + const vaultInfoMap = new Map< + string, + { + name: string; + address: string; + members: number; + minSigners: number; + configurable: string; + version: string; + amountInUSD: number; + } + >(); + + for (const vault of vaultStructures) { + const { members, minSigners } = this.parseVaultSigners(vault.configurable); + vaultInfoMap.set(vault.id, { + name: vault.name, + address: vault.predicateAddress, + members, + minSigners, + configurable: vault.configurable, + version: vault.version, + amountInUSD: 0, + }); + } + + const vaultIds = Array.from(vaultInfoMap.keys()); + + if (vaultIds.length === 0) { + return { data: [], totalAmountInUSD: 0, predicates: [] }; + } + + // ======================================== + // PARALLEL: Fetch reserved coins and balances + // ======================================== + const transactionsQuery = Predicate.createQueryBuilder('p') + .leftJoin( + 'p.transactions', + 't', + "t.status IN (:...status) AND regexp_replace(t.network->>'url', '^https?://[^@]+@', 'https://') = :network", + { + status: [ + TransactionStatus.AWAIT_REQUIREMENTS, + TransactionStatus.PENDING_SENDER, + ], + network: network.url.replace(/^https?:\/\/[^@]+@/, 'https://'), + }, + ) + .where('p.id IN (:...vaultIds)', { vaultIds }) + .select(['p.id', 't.txData']); + + // Fetch reserved coins query (runs in parallel with balance fetches) + const reservedCoinsPromise = transactionsQuery + .getMany() + .then(predicatesWithTx => { + const map = new Map>(); + for (const pred of predicatesWithTx) { + map.set(pred.id, calculateReservedCoins(pred.transactions)); + } + return map; + }); + + // Fetch all balances in parallel with error handling per vault + const balancesPromise = Promise.all( + Array.from(vaultInfoMap.entries()).map(async ([vaultId, info]) => { + try { + const instance = await this.instancePredicate( + info.configurable, + network.url, + info.version, + ); + const balances = (await instance.getBalances()).balances.filter(a => + a.amount.gt(0), + ); + return { vaultId, balances }; + } catch (err) { + logger.warn( + { vaultId, error: err?.message }, + '[ALLOCATION] Failed to get balances for vault', + ); + return { vaultId, balances: [] }; + } + }), + ); + + // Wait for both to complete + const [reservedCoinsMap, vaultBalances] = await Promise.all([ + reservedCoinsPromise, + balancesPromise, + ]); + + // ======================================== + // Process balances and build allocation + // ======================================== + const calculateBalanceUSD = (assetId: string, amount: BN): number => { + try { + const units = fuelUnitAssets(network.chainId, assetId); + const formattedAmount = amount.format({ units }).replace(/,/g, ''); + const priceUSD = quotes[assetId] ?? 0; + return parseFloat(formattedAmount) * priceUSD; + } catch (err) { + logger.warn( + { assetId, error: err?.message }, + '[ALLOCATION] Error calculating USD for asset', + ); + return 0; + } + }; + + const allocationMap = new Map(); + let totalAmountInUSD = 0; + + for (const { vaultId, balances } of vaultBalances) { + const vaultInfo = vaultInfoMap.get(vaultId); + if (!vaultInfo) continue; + + const reservedCoins = reservedCoinsMap.get(vaultId) || []; + const assets = + reservedCoins.length > 0 ? subCoins(balances, reservedCoins) : balances; + + // Filter out NFTs + const assetsWithoutNFT = assets.filter(({ amount, assetId }) => { + const hasFuelMapped = assetsMap[assetId]; + const isOneUnit = amount.eq(1); + return !(!hasFuelMapped && isOneUnit); + }); + + for (const { assetId, amount } of assetsWithoutNFT) { + const usdBalance = calculateBalanceUSD(assetId, amount); + totalAmountInUSD += usdBalance; + vaultInfo.amountInUSD += usdBalance; + + const existing = allocationMap.get(assetId); + allocationMap.set(assetId, { + assetId, + amountInUSD: existing ? existing.amountInUSD + usdBalance : usdBalance, + amount: existing ? existing.amount.add(amount) : amount, + percentage: 0, + }); + } + } + + return this.buildAllocationResponse( + vaultInfoMap, + allocationMap, + totalAmountInUSD, + limit, + ); + } catch (error) { + logger.error( + { + message: error?.message || error, + stack: error?.stack, + userId: user?.id, + predicateId, + networkUrl: network?.url, + }, + '[ALLOCATION_ERROR]', + ); + throw new Internal({ + type: ErrorTypes.Internal, + title: 'Error on get predicate allocation', + detail: error?.message || error, + }); + } + } + + async checkBalances({ + predicateId, + userId, + network, + }: { + predicateId: string; + userId: string; + network: Network; + }): Promise { + try { + const predicate = await Predicate.createQueryBuilder('p') + .leftJoinAndSelect('p.workspace', 'workspace') + .select([ + 'p.id', + 'p.predicateAddress', + 'p.configurable', + 'p.version', + 'workspace.id', + ]) + .where('p.id = :predicateId', { predicateId }) + .getOne(); + + if (!predicate) { + throw new NotFound({ + type: ErrorTypes.NotFound, + title: 'Predicate not found', + detail: `No predicate found with id ${predicateId}`, + }); + } + + const balanceCache = BalanceCache.getInstance(); + const instance = await this.instancePredicate( + predicate.configurable, + network.url, + predicate.version, + ); + + // Only proceed if provider is ProviderWithCache + if (!(instance.provider instanceof ProviderWithCache)) { + return; + } + + // Get cached balance + const cachedBalances = await balanceCache.get( + predicate.predicateAddress, + network.chainId, + ); + + // Get current balance directly from blockchain (bypass cache) + const currentBalances = ( + await (instance.provider as ProviderWithCache).getBalancesFromBlockchain( + predicate.predicateAddress, + ) + ).balances.filter(a => a.amount.gt(0)); + + if (cachedBalances) { + const _cachedBalances = cachedBalances.filter(a => a.amount.gt(0)); + + const hasChanged = compareBalances(_cachedBalances, currentBalances); + + if (hasChanged) { + // Update cache with fresh data + await balanceCache.set( + predicate.predicateAddress, + currentBalances, + network.chainId, + network.url, + ); + + // Invalidate transaction cache + const transactionCache = TransactionCache.getInstance(); + await transactionCache.invalidate( + predicate.predicateAddress, + network.chainId, + ); + + // Emit event to notify balance change + emitBalanceOutdatedPredicate(userId, { + sessionId: userId, + to: SocketUsernames.UI, + type: SocketEvents.BALANCE_OUTDATED_PREDICATE, + predicateId: predicate.id, + workspaceId: predicate.workspace.id, + }); + } + } + } catch (error) { + if (error instanceof NotFound) { + throw error; + } + + throw new Internal({ + type: ErrorTypes.Internal, + title: 'Error on check predicate balance', + detail: error?.message || error, + }); + } + } } diff --git a/packages/api/src/modules/predicate/types.ts b/packages/api/src/modules/predicate/types.ts index 4046829c5..9dabed1d7 100644 --- a/packages/api/src/modules/predicate/types.ts +++ b/packages/api/src/modules/predicate/types.ts @@ -6,8 +6,9 @@ import { IDefaultOrdination } from '@src/utils/ordination'; import { IPagination, PaginationParams } from '@src/utils/pagination'; import { Predicate, User } from '@models/index'; +import { IAssetMapById } from '@src/utils'; +import { BN, Network } from 'fuels'; import { IPredicateOrdination } from './ordination'; -import { Network } from 'fuels'; export enum OrderBy { name = 'name', @@ -69,6 +70,34 @@ export interface IEndCursorPayload { }; }; } +export interface AssetAllocation { + assetId: string | null; // null for "others" + amount: BN; + amountInUSD: number; + percentage: number; +} +export interface PredicateAllocationInfo { + id: string; + name: string; + address: string; + members: number; // total signers count + minSigners: number; // required signatures + amountInUSD: number; +} + +export interface IPredicateAllocation { + data: AssetAllocation[]; + totalAmountInUSD: number; + predicates: PredicateAllocationInfo[]; +} + +export interface IPredicateAllocationParams { + user: User; + predicateId?: string; + network: Network; + assetsMap: IAssetMapById; + limit?: number; // Max number of vaults to process (ordered by most recent tx) +} interface ICreatePredicateRequestSchema extends ValidatedRequestSchema { [ContainerTypes.Body]: IPredicatePayload; @@ -79,8 +108,8 @@ interface ITooglePredicateRequestSchema extends ValidatedRequestSchema { } interface IUpdatePredicateRequestSchema extends ValidatedRequestSchema { - [ContainerTypes.Body]: IPredicatePayload; - [ContainerTypes.Params]: { id: string }; + [ContainerTypes.Body]: Pick; + [ContainerTypes.Params]: { predicateId: string }; } interface IDeletePredicateRequestSchema extends ValidatedRequestSchema { @@ -98,6 +127,9 @@ interface IFindByNameRequestSchema extends ValidatedRequestSchema { [ContainerTypes.Params]: { name: string; }; + [ContainerTypes.Query]: { + ignoreId?: string; + }; } interface IListRequestSchema extends ValidatedRequestSchema { @@ -113,7 +145,20 @@ interface IListRequestSchema extends ValidatedRequestSchema { sort: Sort; page: string; perPage: string; - hidden?: boolean; + hidden?: string; + d?: string; + }; +} + +interface IGetAllocationRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Params]: { + predicateId: string; + }; +} + +interface ICheckPredicateBalancesRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Params]: { + predicateId: string; }; } @@ -139,6 +184,8 @@ export type PredicateWithHidden = Omit< > & { isHidden: boolean; }; +export type IGetAllocationRequest = AuthValidatedRequest; +export type ICheckPredicateBalancesRequest = AuthValidatedRequest; export interface IPredicateService { ordination(ordination?: IPredicateOrdination): this; @@ -151,7 +198,7 @@ export interface IPredicateService { user: User, workspace: Workspace, ) => Promise; - update: (id: string, payload: IPredicatePayload) => Promise; + update: (id: string, payload?: Partial) => Promise; delete: (id: string) => Promise; findById: (id: string, signer?: string) => Promise; list: () => Promise | Predicate[]>; @@ -167,4 +214,10 @@ export interface IPredicateService { address: string, authorization: string, ) => Promise; + allocation: (params: IPredicateAllocationParams) => Promise; + checkBalances: (params: { + predicateId: string; + userId: string; + network: Network; + }) => Promise; } diff --git a/packages/api/src/modules/predicate/validations.ts b/packages/api/src/modules/predicate/validations.ts index 0605b5de9..89fb57051 100644 --- a/packages/api/src/modules/predicate/validations.ts +++ b/packages/api/src/modules/predicate/validations.ts @@ -1,5 +1,5 @@ -import Joi from 'joi'; import { AddressValidator, validator } from '@utils/index'; +import Joi from 'joi'; export const validateAddPredicatePayload = validator.body( Joi.object({ @@ -11,6 +11,13 @@ export const validateAddPredicatePayload = validator.body( }), ); +export const validateUpdatePredicatePayload = validator.body( + Joi.object({ + name: Joi.string().required(), + description: Joi.string().allow('').optional(), + }), +); + export const validatePredicateIdParams = validator.params( Joi.object({ predicateId: Joi.string().uuid().required(), diff --git a/packages/api/src/modules/rampTransactions/controller.ts b/packages/api/src/modules/rampTransactions/controller.ts new file mode 100644 index 000000000..7100d0287 --- /dev/null +++ b/packages/api/src/modules/rampTransactions/controller.ts @@ -0,0 +1,20 @@ +import { bindMethods, successful } from '@src/utils'; +import { error } from '@src/utils/error'; +import { IFindRampTransactionByIdRequest, IRampTransactionService } from './types'; +import { formatRampTransactionResponse } from './utils'; + +export default class RampTransactionsController { + constructor(private rampTransactionService: IRampTransactionService) { + bindMethods(this); + } + + async findById(request: IFindRampTransactionByIdRequest) { + try { + const { id } = request.params; + const transaction = await this.rampTransactionService.findById(id); + return successful(formatRampTransactionResponse(transaction), 200); + } catch (err) { + return error(err.error, err.statusCode); + } + } +} diff --git a/packages/api/src/modules/rampTransactions/routes.ts b/packages/api/src/modules/rampTransactions/routes.ts new file mode 100644 index 000000000..7d808dc0e --- /dev/null +++ b/packages/api/src/modules/rampTransactions/routes.ts @@ -0,0 +1,16 @@ +import { authMiddleware } from '@src/middlewares'; +import { handleResponse } from '@src/utils'; +import { Router } from 'express'; +import RampTransactionsController from './controller'; +import RampTransactionsService from './service'; + +const service = new RampTransactionsService(); +const controller = new RampTransactionsController(service); + +const router = Router(); + +router.use(authMiddleware); + +router.get('/:id', handleResponse(controller.findById)); + +export default router; diff --git a/packages/api/src/modules/rampTransactions/service.ts b/packages/api/src/modules/rampTransactions/service.ts new file mode 100644 index 000000000..62734a165 --- /dev/null +++ b/packages/api/src/modules/rampTransactions/service.ts @@ -0,0 +1,52 @@ +import { RampTransaction } from '@src/models/RampTransactions'; +import { ErrorTypes, Internal, NotFound } from '@src/utils/error'; +import { ICreatePayload, IRampTransactionService } from './types'; + +export default class RampTransactionsService implements IRampTransactionService { + async create(payload: ICreatePayload): Promise { + try { + return await RampTransaction.create({ + provider: payload.provider, + providerData: payload.providerData, + user: payload.user, + transaction: payload.transaction, + sourceCurrency: payload.sourceCurrency, + sourceAmount: payload.sourceAmount, + destinationCurrency: payload.destinationCurrency, + destinationAmount: payload.destinationAmount, + paymentMethod: payload.paymentMethod, + userWalletAddress: payload.userWalletAddress, + isSandbox: payload.isSandbox, + }).save(); + } catch (error) { + throw new Internal({ + title: 'Error creating Ramp Transaction', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + } + } + + async findById(id: string): Promise { + return RampTransaction.findOne({ + where: { id }, + }) + .then(res => { + if (!res) { + throw new NotFound({ + title: 'Ramp Transaction not found', + detail: `No Ramp Transaction found with id ${id}`, + type: ErrorTypes.NotFound, + }); + } + return res; + }) + .catch(error => { + throw new Internal({ + title: 'Error fetching Ramp Transaction', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + }); + } +} diff --git a/packages/api/src/modules/rampTransactions/types.ts b/packages/api/src/modules/rampTransactions/types.ts new file mode 100644 index 000000000..e35e9664b --- /dev/null +++ b/packages/api/src/modules/rampTransactions/types.ts @@ -0,0 +1,35 @@ +import { AuthValidatedRequest } from '@src/middlewares/auth/types'; +import { Transaction, User } from '@src/models'; +import { + ProviderData, + RampTransaction, + RampTransactionProvider, +} from '@src/models/RampTransactions'; +import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; + +export interface ICreatePayload { + provider: RampTransactionProvider; + providerData: ProviderData; + transaction?: Transaction; + user: User; + sourceCurrency?: string; + sourceAmount?: string; + destinationCurrency?: string; + destinationAmount?: string; + paymentMethod?: string; + userWalletAddress?: string; + isSandbox: boolean; +} + +export interface IRampTransactionService { + create: (payload: ICreatePayload) => Promise; + findById: (id: string) => Promise; +} + +interface IFindByIdRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Params]: { + id: string; + }; +} + +export type IFindRampTransactionByIdRequest = AuthValidatedRequest; diff --git a/packages/api/src/modules/rampTransactions/utils.ts b/packages/api/src/modules/rampTransactions/utils.ts new file mode 100644 index 000000000..3dc147de9 --- /dev/null +++ b/packages/api/src/modules/rampTransactions/utils.ts @@ -0,0 +1,22 @@ +import { + RampTransaction, + RampTransactionProvider, +} from '@src/models/RampTransactions'; + +export const formatRampTransactionWithMeldProvider = (data: RampTransaction) => { + return { + id: data.id, + provider: data.provider, + widgetUrl: data.providerData.widgetSessionData.widgetUrl, + status: data.providerData.paymentStatus, + createdAt: data.createdAt, + updatedAt: data.updatedAt, + }; +}; + +export const formatRampTransactionResponse = (data: RampTransaction) => { + if (data.provider === RampTransactionProvider.MELD) { + return formatRampTransactionWithMeldProvider(data); + } + return data; +}; diff --git a/packages/api/src/modules/recoverCode/types.ts b/packages/api/src/modules/recoverCode/types.ts index 5985d9aac..9911fd489 100644 --- a/packages/api/src/modules/recoverCode/types.ts +++ b/packages/api/src/modules/recoverCode/types.ts @@ -1,12 +1,13 @@ -import { RecoverCode, RecoverCodeType, User } from '@src/models'; +import { RecoverCode, RecoverCodeType, User, DApp } from '@src/models'; import { Network } from 'fuels'; +import { Request } from 'express'; export interface ICreateRecoverCodePayload { owner: User; type: RecoverCodeType; origin: string; validAt: Date; - metadata?: { [key: string]: any }; + metadata?: { [key: string]: string | number | boolean | Record }; network: Network; } @@ -15,3 +16,11 @@ export interface IRecoverCodeService { update: (id: string, payload: Partial) => Promise; findByCode: (code: string) => Promise; } + +export interface ICreateRecoverCodeRequest extends Request { + body: Record; + headers: Record; + params: Record; + user?: User; + dapp?: DApp; +} diff --git a/packages/api/src/modules/transaction/controller.ts b/packages/api/src/modules/transaction/controller.ts index 49a4e6bd7..2fac6e578 100644 --- a/packages/api/src/modules/transaction/controller.ts +++ b/packages/api/src/modules/transaction/controller.ts @@ -1,13 +1,9 @@ -import { PermissionRoles, Workspace } from '@src/models/Workspace'; -import { - Unauthorized, - UnauthorizedErrorTitles, -} from '@src/utils/error/Unauthorized'; -import { validatePermissionGeneral } from '@src/utils/permissionValidate'; +import { Workspace } from '@src/models/Workspace'; +import { logger } from '@src/config/logger'; import { TransactionStatus, TransactionType, WitnessStatus } from 'bakosafe'; import { isUUID } from 'class-validator'; -import { NotificationTitle, Predicate, Transaction } from '@models/index'; +import { NotificationTitle, Predicate, Transaction, User } from '@models/index'; import { IPredicateService } from '@modules/predicate/types'; @@ -31,7 +27,7 @@ import { IAddressBookService } from '../addressBook/types'; import { NotificationService } from '../notification/services'; import { INotificationService } from '../notification/types'; import { PredicateService } from '../predicate/services'; -import { UserService } from '../user/service'; + import { WorkspaceService } from '../workspace/services'; import { TransactionService } from './services'; import { @@ -39,6 +35,7 @@ import { ICloseTransactionRequest, ICreateTransactionHistoryRequest, ICreateTransactionRequest, + IDeleteTransactionByHashRequest, IFindTransactionByHashRequest, IFindTransactionByIdRequest, IListRequest, @@ -81,32 +78,58 @@ export class TransactionController { // pending tx async pending(req: IListRequest) { try { - const { workspace, user, network } = req; + const { user, network } = req; const { predicateId } = req.query; const predicate = predicateId && predicateId.length > 0 ? predicateId[0] : undefined; + // Use chainId for filtering (faster than URL regex, uses index) + const chainId = String(network.chainId); + if (!predicate) { - const qb = Transaction.createQueryBuilder('t') - .innerJoinAndSelect('t.predicate', 'pred') - .innerJoin('pred.workspace', 'wks', 'wks.id = :workspaceId', { - workspaceId: workspace.id, - }) - .addSelect(['t.status', 't.resume']) - .where('t.status = :status', { + // Query 1: Get count of pending transactions (fast, no data transfer) + const countQb = Transaction.createQueryBuilder('t') + .select('COUNT(DISTINCT t.id)', 'count') + .innerJoin('t.predicate', 'pred') + .leftJoin('pred.members', 'pm') + .leftJoin('pred.owner', 'owner') + .where('(pm.id = :userId OR owner.id = :userId)', { userId: user.id }) + .andWhere('t.status = :status', { status: TransactionStatus.AWAIT_REQUIREMENTS, }) - .andWhere( - // TODO: On release to mainnet we need to remove this condition - `regexp_replace(t.network->>'url', '^https?://[^@]+@', 'https://') = :network`, + .andWhere(`t.network->>'chainId' = :chainId`, { chainId }); + + const countResult = await countQb.getRawOne(); + const ofUser = Number(countResult?.count ?? 0); + + // Early return if no pending transactions + if (ofUser === 0) { + return successful( { - network: network.url.replace(/^https?:\/\/[^@]+@/, 'https://'), + ofUser: 0, + transactionsBlocked: false, + pendingSignature: false, }, + Responses.Ok, ); + } + + // Query 2: Check if user has pending signature (only fetch resume) + const pendingSignatureQb = Transaction.createQueryBuilder('t') + .select(['t.id', 't.resume']) + .distinctOn(['t.id']) + .innerJoin('t.predicate', 'pred') + .leftJoin('pred.members', 'pm') + .leftJoin('pred.owner', 'owner') + .where('(pm.id = :userId OR owner.id = :userId)', { userId: user.id }) + .andWhere('t.status = :status', { + status: TransactionStatus.AWAIT_REQUIREMENTS, + }) + .andWhere(`t.network->>'chainId' = :chainId`, { chainId }) + .orderBy('t.id', 'ASC'); - const transactions = await qb.getMany(); + const transactions = await pendingSignatureQb.getMany(); - const ofUser = transactions.length; const pendingSignature = transactions.some(tx => tx.resume?.witnesses?.some( w => w.account === user.address && !w.signature, @@ -123,19 +146,14 @@ export class TransactionController { ); } + // With predicateId filter const qb = Transaction.createQueryBuilder('t') - .addSelect(['t.resume']) + .select(['t.id', 't.resume']) .where('t.status = :status', { status: TransactionStatus.AWAIT_REQUIREMENTS, }) .andWhere('t.predicateId = :predicate', { predicate }) - .andWhere( - // TODO: On release to mainnet we need to remove this condition - `regexp_replace(t.network->>'url', '^https?://[^@]+@', 'https://') = :network`, - { - network: network.url.replace(/^https?:\/\/[^@]+@/, 'https://'), - }, - ); + .andWhere(`t.network->>'chainId' = :chainId`, { chainId }); const transactions = await qb.getMany(); @@ -165,6 +183,16 @@ export class TransactionController { }: ICreateTransactionRequest) { const { predicateAddress, summary, hash } = transaction; + logger.info( + { + predicateAddress, + hash, + userId: user?.id, + networkUrl: network?.url, + }, + '[TX_CREATE] Starting transaction creation', + ); + try { const existsTx = await Transaction.findOne({ where: { @@ -180,33 +208,63 @@ export class TransactionController { }); if (existsTx) { + logger.info( + { data: { hash, txId: existsTx.id } }, + '[TX_CREATE] Transaction already exists', + ); return successful(existsTx, Responses.Ok); } - const predicate = await new PredicateService() - .filter({ address: predicateAddress }) - .list() - .then((result: Predicate[]) => result[0]); + const predicate = await this.predicateService.findByAddress(predicateAddress); + logger.info( + { + found: !!predicate, + predicateId: predicate?.id, + predicateName: predicate?.name, + }, + '[TX_CREATE] Predicate search result', + ); // if possible move this next part to a middleware, but we dont have access to body of request // ======================================================================================================== - const hasPermission = validatePermissionGeneral(workspace, user.id, [ - PermissionRoles.OWNER, - PermissionRoles.ADMIN, - PermissionRoles.MANAGER, - ]); - const isMemberOfPredicate = predicate.members.find( - member => member.id === user.id, - ); + // const hasPermission = validatePermissionGeneral(workspace, user.id, [ + // PermissionRoles.OWNER, + // PermissionRoles.ADMIN, + // PermissionRoles.MANAGER, + // ]); + // const isMemberOfPredicate = predicate.members.find( + // member => member.id === user.id, + // ); + + // if (!isMemberOfPredicate && !hasPermission) { + // throw new Unauthorized({ + // type: ErrorTypes.Unauthorized, + // title: UnauthorizedErrorTitles.MISSING_PERMISSION, + // detail: 'You do not have permission to access this resource', + // }); + // } + // ======================================================================================================== - if (!isMemberOfPredicate && !hasPermission) { - throw new Unauthorized({ - type: ErrorTypes.Unauthorized, - title: UnauthorizedErrorTitles.MISSING_PERMISSION, - detail: 'You do not have permission to access this resource', + if (!predicate) { + logger.info( + { predicateAddress }, + '[TX_CREATE] ERROR: Predicate not found for address', + ); + throw new BadRequest({ + type: ErrorTypes.NotFound, + title: 'Predicate not found', + detail: `No predicate found with address ${predicateAddress}`, }); } - // ======================================================================================================== + + logger.info( + { + predicateId: predicate.id, + predicateName: predicate.name, + membersCount: predicate.members?.length, + }, + '[TX_CREATE] Predicate found', + ); const witnesses = predicate.members.map(member => ({ account: member.address, @@ -270,7 +328,7 @@ export class TransactionController { vaultName: predicate.name, transactionName: name, transactionId: id, - workspaceId: predicate.workspace.id, + workspaceId: predicate.workspace?.id, }, user_id: member.id, network, @@ -293,6 +351,15 @@ export class TransactionController { return successful(newTransaction, Responses.Created); } catch (e) { + logger.error( + { + message: e?.message || e?.error?.detail || e, + type: e?.error?.type, + title: e?.error?.title, + stack: e?.stack?.slice(0, 500), + }, + '[TX_CREATE]', + ); return error(e.error, e.statusCode); } } @@ -322,8 +389,6 @@ export class TransactionController { } static async formatTransactionsHistory(data: Transaction) { - const userService = new UserService(); - const events = [ createTxHistoryEvent( TransactionHistory.CREATED, @@ -339,18 +404,26 @@ export class TransactionController { witness.status === WitnessStatus.CANCELED, ); + // Batch fetch all users at once instead of N individual queries + const witnessAddresses = _witnesses.map(w => w.account); + const users = + witnessAddresses.length > 0 + ? await User.find({ where: { address: In(witnessAddresses) } }) + : []; + const userMap = new Map(users.map(u => [u.address, u])); + const witnessEventMap = { [WitnessStatus.DONE]: TransactionHistory.SIGN, [WitnessStatus.REJECTED]: TransactionHistory.DECLINE, [WitnessStatus.CANCELED]: TransactionHistory.CANCEL, }; - const witnessEvents = await Promise.all( - _witnesses.map(async witness => { - const user = await userService.findByAddress(witness.account); - const eventType = witnessEventMap[witness.status]; - return createTxHistoryEvent(eventType, witness.updatedAt, user); - }), - ); + + // Use pre-fetched users from map (O(1) lookup instead of DB call) + const witnessEvents = _witnesses.map(witness => { + const user = userMap.get(witness.account); + const eventType = witnessEventMap[witness.status]; + return createTxHistoryEvent(eventType, witness.updatedAt, user); + }); events.push(...witnessEvents); @@ -436,12 +509,13 @@ export class TransactionController { try { const transaction = await Transaction.findOne({ where: { - hash: txHash, + hash: txHash.startsWith(`0x`) ? txHash.slice(2) : txHash, status: Not( In([ TransactionStatus.DECLINED, TransactionStatus.FAILED, TransactionStatus.CANCELED, + TransactionStatus.SUCCESS, ]), ), }, @@ -478,10 +552,10 @@ export class TransactionController { const _transaction = await transaction.save(); - console.log('[SIGN_BY_ID] Transaction status updated: ', { - status: _transaction.status, - resume: _transaction.resume, - }); + logger.info( + { status: _transaction.status }, + '[SIGN_BY_ID] Transaction status updated', + ); if (newStatus === TransactionStatus.PENDING_SENDER) { await this.transactionService.sendToChain(transaction.hash, network); @@ -511,7 +585,7 @@ export class TransactionController { return successful(true, Responses.Ok); } catch (e) { - console.error('[SIGN_BY_ID] Error: ', e); + logger.error({ error: e }, '[SIGN_BY_ID]'); return error(e.error, e.statusCode); } } @@ -673,7 +747,6 @@ export class TransactionController { return successful(response, Responses.Ok); } catch (e) { - console.log(`[INCOMING_ERROR]`, e); return error(e.error, e.statusCode); } } @@ -754,12 +827,26 @@ export class TransactionController { params: { id }, }: ICloseTransactionRequest) { try { + // Get transaction with predicate to invalidate cache + const transaction = await Transaction.findOne({ + where: { id }, + relations: ['predicate'], + }); + const response = await this.transactionService.update(id, { status: TransactionStatus.SUCCESS, sendTime: new Date(), gasUsed, resume: transactionResult, }); + + // Invalidate caches for all predicates involved in this transaction + this.transactionService + .invalidateCaches(transaction) + .catch(err => + logger.error({ error: err }, '[TX_CLOSE] Failed to invalidate caches:'), + ); + return successful(response, Responses.Ok); } catch (e) { return error(e.error, e.statusCode); @@ -774,7 +861,7 @@ export class TransactionController { await this.transactionService.sendToChain(hash.slice(2), params.network); // not wait for this return successful(true, Responses.Ok); } catch (e) { - console.log('[TX_ERROR]', e); + logger.error({ error: e }, '[TX_SEND]'); return error(e.error, e.statusCode); } } @@ -783,7 +870,7 @@ export class TransactionController { try { const { page, perPage } = req.query; const response = await this.transactionService - .paginate({ page: page || 0, perPage: perPage || 30 }) + .paginate({ page: page || '0', perPage: perPage || '30' }) .listAll(); return successful(response, Responses.Ok); } catch (e) { @@ -800,4 +887,16 @@ export class TransactionController { return error(e.error, e.statusCode); } } + + async deleteByHash(req: IDeleteTransactionByHashRequest) { + try { + const { hash } = req.params; + const response = await this.transactionService.deleteByHash( + hash.startsWith(`0x`) ? hash.slice(2) : hash, + ); + return successful(response, Responses.Ok); + } catch (e) { + return error(e.error, e.statusCode); + } + } } diff --git a/packages/api/src/modules/transaction/routes.ts b/packages/api/src/modules/transaction/routes.ts index dae9ed732..58497b117 100644 --- a/packages/api/src/modules/transaction/routes.ts +++ b/packages/api/src/modules/transaction/routes.ts @@ -32,7 +32,9 @@ const wkPermissionMiddleware = workspacePermissionMiddleware({ }); const txPermissionMiddleware = transactionPermissionMiddleware({ - transactionSelector: req => req.params.hash, + transactionSelector: req => { + return req.params.hash; + }, }); const router = Router(); @@ -54,6 +56,7 @@ const { createHistory, cancel, findAdvancedDetails, + deleteByHash, } = new TransactionController( transactionService, predicateService, @@ -86,11 +89,16 @@ router.put('/close/:id', validateCloseTransactionPayload, handleResponse(close)) router.put( '/sign/:hash', - validateSignerByIdPayload, + // validateSignerByIdPayload, txPermissionMiddleware, handleResponse(signByID), ); router.put('/cancel/:hash', txPermissionMiddleware, handleResponse(cancel)); router.get('/history/:id/:predicateId', handleResponse(createHistory)); +router.delete( + '/by-hash/:hash', + txPermissionMiddleware, + handleResponse(deleteByHash), +); export default router; diff --git a/packages/api/src/modules/transaction/services.ts b/packages/api/src/modules/transaction/services.ts index c3d4eb96e..100a41e6c 100644 --- a/packages/api/src/modules/transaction/services.ts +++ b/packages/api/src/modules/transaction/services.ts @@ -1,4 +1,5 @@ import { IWitnesses, TransactionStatus, Vault, WitnessStatus } from 'bakosafe'; +import { logger } from '@src/config/logger'; import { Address, bn, @@ -8,7 +9,7 @@ import { OutputType, transactionRequestify, } from 'fuels'; -import { Brackets, In, Not } from 'typeorm'; +import { Brackets, EntityNotFoundError, In, Not } from 'typeorm'; import { Predicate, Transaction } from '@models/index'; @@ -16,9 +17,11 @@ import { NotFound } from '@utils/error'; import GeneralError, { ErrorTypes } from '@utils/error/GeneralError'; import Internal from '@utils/error/Internal'; import { IOrdination, setOrdination } from '@utils/ordination'; + import { IPagination, Pagination, PaginationParams } from '@utils/pagination'; -import { FuelProvider } from '@src/utils'; +import { extractPredicatesFromTransaction, FuelProvider } from '@src/utils'; +import App from '@src/server/app'; import { NotificationService } from '../notification/services'; import { TransactionPagination, TransactionPaginationParams } from './pagination'; import { @@ -125,9 +128,7 @@ export class TransactionService implements ITransactionService { } async findById(id: string): Promise { - console.log('[FIND_BY_ID] Finding transaction by ID: ', { - id, - }); + logger.info({ id }, '[FIND_BY_ID] Finding transaction by ID'); return await Transaction.findOne({ where: { id }, @@ -136,6 +137,7 @@ export class TransactionService implements ITransactionService { 'predicate.members', 'predicate.workspace', 'createdBy', + 'rampTransaction', ], }) .then(transaction => { @@ -184,6 +186,7 @@ export class TransactionService implements ITransactionService { .leftJoin('t.predicate', 'predicate') .leftJoin('predicate.members', 'members') .leftJoin('predicate.workspace', 'workspace') + .leftJoin('t.rampTransaction', 'ramp') .addSelect([ 'predicate.name', 'predicate.id', @@ -194,6 +197,14 @@ export class TransactionService implements ITransactionService { 'workspace.id', 'workspace.name', 'workspace.single', + 'ramp.id', + 'ramp.provider', + 'ramp.sourceCurrency', + 'ramp.sourceAmount', + 'ramp.destinationCurrency', + 'ramp.destinationAmount', + 'ramp.paymentMethod', + 'ramp.providerData', ]) .andWhere( // TODO: On release to mainnet we need to remove this condition @@ -298,12 +309,15 @@ export class TransactionService implements ITransactionService { }); }; + // Apply default limit to prevent loading entire table when no pagination + const DEFAULT_LIMIT = 100; const transactions = hasPagination ? await Pagination.create(queryBuilder) .paginate(this._pagination) .then(paginationResult => paginationResult) .catch(handleInternalError) : await queryBuilder + .take(DEFAULT_LIMIT) .getMany() .then(transactions => { return transactions ?? []; @@ -338,6 +352,7 @@ export class TransactionService implements ITransactionService { .leftJoin('t.predicate', 'predicate') .leftJoin('predicate.members', 'members') .leftJoin('predicate.workspace', 'workspace') + .leftJoin('t.rampTransaction', 'ramp') .addSelect([ 'predicate.name', 'predicate.id', @@ -348,6 +363,14 @@ export class TransactionService implements ITransactionService { 'workspace.id', 'workspace.name', 'workspace.single', + 'ramp.id', + 'ramp.provider', + 'ramp.sourceCurrency', + 'ramp.sourceAmount', + 'ramp.destinationCurrency', + 'ramp.destinationAmount', + 'ramp.paymentMethod', + 'ramp.providerData', ]) .andWhere( `regexp_replace(t.network->>'url', '^https?://[^@]+@', 'https://') = :network`, @@ -430,12 +453,38 @@ export class TransactionService implements ITransactionService { }); } + async deleteByHash(hash: string): Promise { + try { + const tx = await Transaction.findOneOrFail({ + where: { hash, status: TransactionStatus.AWAIT_REQUIREMENTS }, + }); + + tx.deletedAt = new Date(); + await tx.save(); + + return true; + } catch (e) { + if (e instanceof EntityNotFoundError) { + throw new NotFound({ + type: ErrorTypes.NotFound, + title: 'Transaction not found', + detail: `Transaction with hash ${hash} and status ${TransactionStatus.AWAIT_REQUIREMENTS} not found`, + }); + } + + throw new Internal({ + type: ErrorTypes.Internal, + title: 'Error on transaction delete', + detail: e, + }); + } + } + async findAdvancedDetailById(id: string): Promise { const transaction = await Transaction.findOne({ where: { id }, relations: { predicate: true }, }); - if (!transaction) { throw new NotFound({ type: ErrorTypes.NotFound, @@ -549,10 +598,7 @@ export class TransactionService implements ITransactionService { //instance tx //add witnesses async sendToChain(hash: string, network: Network) { - console.log('[SEND_TO_CHAIN] Sending transaction to chain: ', { - hash, - network, - }); + logger.info({ hash, network }, '[SEND_TO_CHAIN] Sending transaction to chain'); const transaction = await Transaction.findOne({ where: { @@ -569,11 +615,14 @@ export class TransactionService implements ITransactionService { relations: ['predicate', 'createdBy'], }); - console.log('[SEND_TO_CHAIN] Transaction data: ', { - hash, - transaction: !!transaction, - status: transaction?.status, - }); + logger.info( + { + hash, + transaction: !!transaction, + status: transaction?.status, + }, + '[SEND_TO_CHAIN] Found transaction', + ); if (!transaction) { throw new NotFound({ @@ -585,13 +634,6 @@ export class TransactionService implements ITransactionService { const { id, predicate, txData, status, resume } = transaction; - console.log('[SEND_TO_CHAIN] Transaction data: ', { - id: id, - status, - resume, - txData, - }); - if (status != TransactionStatus.PENDING_SENDER) { return await this.findById(id); } @@ -606,22 +648,20 @@ export class TransactionService implements ITransactionService { predicate.version, ); + const w = transaction.getWitnesses(); + const tx = transactionRequestify({ ...txData, - witnesses: transaction.getWitnesses(), + witnesses: w, }); - console.log('[SEND_TO_CHAIN] Transaction request: ', { - tx, - }); + logger.info({ tx }, '[SEND_TO_CHAIN] Transaction request'); try { const transactionResponse = await vault.send(tx); const { gasUsed } = await transactionResponse.waitForResult(); - console.log('[SEND_TO_CHAIN] Transaction response: ', { - gasUsed, - }); + logger.info({ gasUsed }, '[SEND_TO_CHAIN] Transaction response'); const _api_transaction: IUpdateTransactionPayload = { status: TransactionStatus.SUCCESS, @@ -636,10 +676,14 @@ export class TransactionService implements ITransactionService { await new NotificationService().transactionSuccess(id, network); + // Invalidate caches for all predicates involved in this transaction + this.invalidateCaches(transaction).catch(err => + logger.error({ error: err }, '[TX_SUCCESS] Failed to invalidate caches'), + ); + return await this.update(id, _api_transaction); } catch (e) { - console.error('[SEND_TO_CHAIN] Error: ', e); - + logger.error({ error: e }, '[SEND_TO_CHAIN]'); const error = 'toObject' in e ? e.toObject() : e; const _api_transaction: IUpdateTransactionPayload = { status: TransactionStatus.FAILED, @@ -663,16 +707,39 @@ export class TransactionService implements ITransactionService { try { let _transactions: ITransactionResponse[] = []; + const provider = await FuelProvider.create(providerUrl); + const chainId = await FuelProvider.getChainId(providerUrl); + const transactionCache = App.getInstance()._transactionCache; + for await (const predicate of predicates) { const address = Address.fromString(predicate.predicateAddress).toB256(); - const provider = await FuelProvider.create(providerUrl); - // TODO: change this to use pagination and order DESC + // Check cache with refresh status + const cacheResult = await transactionCache.getWithRefreshCheck( + address, + chainId, + ); + + if (!cacheResult.needsIncrementalFetch) { + // Cache is fresh, use it directly + _transactions = [ + ..._transactions, + ...((cacheResult.cachedTransactions as unknown) as ITransactionResponse[]), + ]; + continue; + } + + // Need to fetch from blockchain (full or incremental) + const fetchLimit = + cacheResult.cachedTransactions.length > 0 + ? transactionCache.getIncrementalFetchLimit() // Incremental: fetch only recent + : 57; // Full fetch + const { transactions } = await getTransactionsSummaries({ provider, filters: { owner: address, - first: 57, + first: fetchLimit, }, }); @@ -681,19 +748,36 @@ export class TransactionService implements ITransactionService { .filter(tx => tx.isStatusSuccess) .filter(tx => tx.operations.some(op => op.to?.address === address)); - // formatFueLTransactio needs to be async because of the request to get fuels tokens up to date and use them to get the network units + // Format transactions const formattedTransactions = await Promise.all( filteredTransactions.map(tx => formatFuelTransaction(tx, predicate, provider), ), ); - _transactions = [..._transactions, ...formattedTransactions]; + // Merge with cached if incremental, otherwise use fresh data + let finalTransactions: ITransactionResponse[]; + if (cacheResult.cachedTransactions.length > 0) { + // Incremental merge - cast to any to handle generic type + const cachedTxs = cacheResult.cachedTransactions as ITransactionResponse[]; + finalTransactions = transactionCache.mergeTransactions( + cachedTxs, + formattedTransactions, + cacheResult.knownHashes, + ); + } else { + // Full fetch + finalTransactions = formattedTransactions; + } + + // Update cache + await transactionCache.set(address, finalTransactions, chainId); + + _transactions = [..._transactions, ...finalTransactions]; } return _transactions; } catch (e) { - console.log('[ERROR] fetchFuelTransactions', e); return []; } } @@ -747,4 +831,71 @@ export class TransactionService implements ITransactionService { return Pagination.create(queryBuilder).paginate(this._pagination); } + + /** + * Invalidate all caches for predicates involved in a transaction + * Called after successful transactions to ensure fresh data for all affected predicates + * + * @param transaction - The transaction object containing summary with operations + */ + async invalidateCaches(transaction: Transaction): Promise { + try { + const predicateAddresses = extractPredicatesFromTransaction(transaction); + + if (predicateAddresses.length === 0) { + logger.info('[TX_CACHE] No predicate addresses found in transaction'); + return; + } + + for (const predicateAddress of predicateAddresses) { + await this.invalidatePredicateCaches( + predicateAddress, + transaction.network?.chainId, + ); + } + + logger.info( + { predicatesCount: predicateAddresses.length }, + '[TX_CACHE] Invalidated caches for predicates', + ); + } catch (error) { + logger.error( + { error: error }, + '[TX_CACHE] Failed to invalidate transaction caches', + ); + } + } + + /** + * Invalidate all caches for a predicate after transaction changes + * Called after successful transactions to ensure fresh data + * + * @param predicateAddress - The predicate address to invalidate + * @param chainId - Optional chainId for granular invalidation (only invalidates that specific chain) + */ + private async invalidatePredicateCaches( + predicateAddress: string, + chainId?: number, + ): Promise { + try { + // Invalidate balance cache + const balanceCache = App.getInstance()._balanceCache; + await balanceCache.invalidate(predicateAddress, chainId); + + // Invalidate transaction cache + const transactionCache = App.getInstance()._transactionCache; + await transactionCache.invalidate(predicateAddress, chainId); + + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + chainId: chainId ?? 'all', + }, + '[TX_CACHE] Caches invalidated', + ); + } catch (error) { + // Don't throw - cache invalidation failure shouldn't break transaction flow + logger.error({ error: error }, '[TX_CACHE] Failed to invalidate caches'); + } + } } diff --git a/packages/api/src/modules/transaction/types.ts b/packages/api/src/modules/transaction/types.ts index b1c2f5f99..6e116c122 100644 --- a/packages/api/src/modules/transaction/types.ts +++ b/packages/api/src/modules/transaction/types.ts @@ -4,11 +4,17 @@ import { ITransferAsset, IWitnesses, TransactionStatus, + TypeUser, } from 'bakosafe'; import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; import { Network, Receipt, TransactionRequest } from 'fuels'; -import { Predicate, Transaction, TransactionType, TypeUser } from '@models/index'; +import { + Predicate, + Transaction, + TransactionStatusWithRamp, + TransactionType, +} from '@models/index'; import { AuthValidatedRequest } from '@middlewares/auth/types'; @@ -59,7 +65,7 @@ export interface ICreateTransactionPayload { name: string; hash: string; predicateAddress: string; - status: TransactionStatus; + status: TransactionStatus | TransactionStatusWithRamp; txData: TransactionRequest; assets: { assetId: string; @@ -149,6 +155,10 @@ interface IDeleteTransactionRequestSchema extends ValidatedRequestSchema { [ContainerTypes.Params]: { id: string }; } +interface IDeleteTransactionByHashRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Params]: { hash: string }; +} + interface ICloseTransactionRequestSchema extends ValidatedRequestSchema { [ContainerTypes.Body]: ICloseTransactionBody; [ContainerTypes.Params]: { id: string }; @@ -224,6 +234,7 @@ export type ICreateTransactionRequest = AuthValidatedRequest; export type IUpdateTransactionRequest = AuthValidatedRequest; export type IDeleteTransactionRequest = AuthValidatedRequest; +export type IDeleteTransactionByHashRequest = AuthValidatedRequest; export type ICloseTransactionRequest = AuthValidatedRequest; export type ICancelTransactionRequest = AuthValidatedRequest; export type ISendTransactionRequest = AuthValidatedRequest; @@ -258,6 +269,7 @@ export interface ITransactionService { findById: (id: string) => Promise; findByHash: (hash: string) => Promise; delete: (id: string) => Promise; + deleteByHash: (hash: string) => Promise; findAdvancedDetailById(id: string): Promise; // graphql @@ -279,4 +291,5 @@ export interface ITransactionService { checkInvalidConditions: (api_transaction: TransactionStatus) => void; validateSignature: (transaction: Transaction, userAddress: string) => boolean; listAll(): Promise>; + invalidateCaches: (transaction: Transaction) => Promise; } diff --git a/packages/api/src/modules/transaction/utils.ts b/packages/api/src/modules/transaction/utils.ts index 14ade0782..5ae2ad5d0 100644 --- a/packages/api/src/modules/transaction/utils.ts +++ b/packages/api/src/modules/transaction/utils.ts @@ -1,23 +1,21 @@ import { Predicate, Transaction, TransactionType, User } from '@src/models'; -import { IPagination } from '@src/utils/pagination'; -import { - ITransactionResponse, - ITransactionsListParams, - TransactionHistory, -} from './types'; -import { TransactionStatus } from 'bakosafe'; -import { Provider, TransactionResult } from 'fuels'; import { formatAssets } from '@src/utils/formatAssets'; import { IDefaultOrdination, IOrdination, Sort, } from '@src/utils/ordination/helper'; +import { IPagination } from '@src/utils/pagination'; +import { TransactionStatus } from 'bakosafe'; import { isUUID } from 'class-validator'; -import { ITransactionCounter } from './types'; +import { Provider, TransactionResult } from 'fuels'; import { ITransactionPagination } from './pagination'; -import { getAssetsMaps } from '@src/utils'; -import { PredicateService } from '../predicate/services'; +import { + ITransactionCounter, + ITransactionResponse, + ITransactionsListParams, + TransactionHistory, +} from './types'; export const formatTransactionsResponse = ( transactions: IPagination | Transaction[], diff --git a/packages/api/src/modules/user/controller.ts b/packages/api/src/modules/user/controller.ts index d550a9523..ac21d9b6b 100644 --- a/packages/api/src/modules/user/controller.ts +++ b/packages/api/src/modules/user/controller.ts @@ -1,47 +1,58 @@ import { addMinutes } from 'date-fns'; -import { Predicate, RecoverCode, RecoverCodeType } from '@src/models'; +import { + Predicate, + RecoverCode, + RecoverCodeType, + TransactionStatus, + TransactionType, +} from '@src/models'; import { User } from '@src/models/User'; import { bindMethods } from '@src/utils/bindMethods'; import { BadRequest, + error, ErrorTypes, Unauthorized, UnauthorizedErrorTitles, - error, } from '@utils/error'; import { IconUtils } from '@utils/icons'; -import { Responses, successful, TokenUtils } from '@utils/index'; +import { getAssetsMaps, Responses, successful, TokenUtils } from '@utils/index'; +import App from '@src/server/app'; +import { FuelProvider } from '@src/utils/FuelProvider'; +import { Not } from 'typeorm'; +import { IChangenetworkRequest } from '../auth/types'; import { PredicateService } from '../predicate/services'; +import { PredicateWithHidden } from '../predicate/types'; import { RecoverCodeService } from '../recoverCode/services'; import { TransactionService } from '../transaction/services'; +import { mergeTransactionLists } from '../transaction/utils'; import { UserService } from './service'; import { + IAllocationRequest, ICheckHardwareRequest, ICheckNicknameRequest, + ICheckUserBalancesRequest, ICreateRequest, IDeleteRequest, IFindByNameRequest, IFindOneRequest, IListRequest, + IListUserTransactionsRequest, IMeInfoRequest, IMeRequest, IUpdateRequest, IUserService, } from './types'; -import { Not } from 'typeorm'; -import App from '@src/server/app'; -import { IChangenetworkRequest } from '../auth/types'; -import { FuelProvider } from '@src/utils/FuelProvider'; -import { PredicateWithHidden } from '../predicate/types'; +import { logger } from '@src/config/logger'; export class UserController { - private userService: IUserService; - - constructor(userService: IUserService) { - this.userService = userService; + constructor( + private userService: IUserService, + private transactionService: TransactionService, + ) { bindMethods(this); } @@ -76,28 +87,61 @@ export class UserController { ); } - async meTransactions(req: IMeRequest) { + async meTransactions(req: IListUserTransactionsRequest) { try { - const { type } = req.query; + const { type, status, offsetDb, offsetFuel, perPage } = req.query; const { user, network } = req; - const transactions = await new TransactionService() + const ordination = { orderBy: 'createdAt', sort: 'DESC' } as const; + + const transactions = await this.transactionService .filter({ type, signer: user.address, network: network.url, + status, }) - .paginate({ page: '0', perPage: '6' }) - .ordination({ orderBy: 'createdAt', sort: 'DESC' }) - .list(); + .transactionPaginate({ offsetDb, offsetFuel, perPage }) + .ordination(ordination) + .listWithIncomings(); + + const shouldFetchFuelTxs = + (type === TransactionType.DEPOSIT || !type) && + (status + ? !status.find(item => item === TransactionStatus.AWAIT_REQUIREMENTS) // dont fetch fuel txs if filtering by AWAIT_REQUIREMENTS status + : true); + + let fuelTxs = []; + if (shouldFetchFuelTxs) { + const predicates = await new PredicateService() + .filter({ + owner: user.id, + }) + // get the latest used predicates + .paginate({ page: '0', perPage: '6' }) + .ordination({ orderBy: 'updatedAt', sort: 'DESC' }) + .list() + .then(res => (Array.isArray(res) ? res : res.data)); + + fuelTxs = await this.transactionService + .transactionPaginate({ + perPage, + offsetDb, + offsetFuel, + }) + .fetchFuelTransactions(predicates, network.url); + } - return successful( - transactions, + const response = mergeTransactionLists(transactions, fuelTxs, { + offsetDb, + offsetFuel, + ordination, + perPage, + }); - Responses.Ok, - ); + return successful(response, Responses.Ok); } catch (e) { - return error(e.error, e.statusCode); + return error(e.error ?? e, e.statusCode); } } @@ -105,23 +149,12 @@ export class UserController { const { user, workspace, network } = req; return successful( { - id: user.id, - name: user.name, - type: user.type, - avatar: user.avatar, - address: user.address, - webauthn: user.webauthn, - first_login: user.first_login, + ...user, network, - settings: user.settings, onSingleWorkspace: workspace.single && workspace.name.includes(`[${user.id}]`), workspace: { - id: workspace.id, - name: workspace.name, - avatar: workspace.avatar, - single: workspace.single, - description: workspace.description, + ...workspace, permission: workspace.permissions[user.id], }, }, @@ -277,7 +310,7 @@ export class UserController { return successful(code, Responses.Created); } catch (e) { - console.log(e); + logger.error({ error: e }, '[USER_CREATE]'); return error(e.error, e.statusCode); } } @@ -363,20 +396,84 @@ export class UserController { async changeNetwork({ user, body }: IChangenetworkRequest) { const { network } = body; - const result = await TokenUtils.changeNetwork(user.id, network); + const updatedNetwork = await TokenUtils.changeNetwork(user.id, network); - return successful(!!result, Responses.Ok); + return successful(updatedNetwork, Responses.Ok); } async listAll(req: IListRequest) { try { const { page, perPage } = req.query; const response = await this.userService - .paginate({ page: page || 0, perPage: perPage || 30 }) + .paginate({ page: page || '0', perPage: perPage || '30' }) .listAll(); return successful(response, Responses.Ok); } catch (e) { return error(e.error, e.statusCode); } } + + async wallet(req: IMeRequest) { + try { + const { user } = req; + + const personalVault = await Predicate.findOne({ + where: { + owner: { id: user.id }, + root: true, + }, + }); + + if (!personalVault) { + return successful( + { + message: 'Personal Account not found', + wallet: null, + }, + Responses.Ok, + ); + } + + return successful( + { + address: personalVault.predicateAddress, + configurable: personalVault.configurable, + version: personalVault.version, + }, + Responses.Ok, + ); + } catch (e) { + return error(e.error, e.statusCode); + } + } + + async allocation({ user, network, query }: IAllocationRequest) { + try { + const { limit } = query; + + const allocation = await new PredicateService().allocation({ + user, + network, + assetsMap: (await getAssetsMaps()).assetsMapById, + limit, + }); + + return successful(allocation, Responses.Ok); + } catch (e) { + return error(e.error ?? e, e.statusCode); + } + } + + async checkUserBalances({ user, workspace, network }: ICheckUserBalancesRequest) { + try { + await this.userService.checkBalances({ + userId: user.id, + workspaceId: workspace.id, + network, + }); + return successful(null, Responses.Ok); + } catch (e) { + return error(e.error || e, e.statusCode); + } + } } diff --git a/packages/api/src/modules/user/routes.ts b/packages/api/src/modules/user/routes.ts index 6858dd645..fba71812a 100644 --- a/packages/api/src/modules/user/routes.ts +++ b/packages/api/src/modules/user/routes.ts @@ -4,18 +4,22 @@ import { authMiddleware } from '@middlewares/index'; import { handleResponse } from '@utils/index'; +import { TransactionService } from '../transaction/services'; import { UserController } from './controller'; import { UserService } from './service'; import { + AllocationQuerySchema, FindUserByIDParams, + ListUserTransactionsQuerySchema, PayloadCreateUserSchema, PayloadUpdateUserSchema, } from './validation'; const router = Router(); const userService = new UserService(); +const transactionService = new TransactionService(); -const userController = new UserController(userService); +const userController = new UserController(userService, transactionService); router.get('/nickname/:nickname', handleResponse(userController.validateName)); router.post('/', PayloadCreateUserSchema, handleResponse(userController.create)); @@ -34,9 +38,13 @@ router.get( authMiddleware, handleResponse(userController.tokensUSDAmount), ); + +router.get('/wallet', authMiddleware, handleResponse(userController.wallet)); + router.get( - '/latest/transactions', + '/transactions', authMiddleware, + ListUserTransactionsQuerySchema, handleResponse(userController.meTransactions), ); router.get( @@ -49,6 +57,17 @@ router.get( authMiddleware, handleResponse(userController.predicates), ); +router.get( + '/allocation', + authMiddleware, + AllocationQuerySchema, + handleResponse(userController.allocation), +); +router.get( + '/check-balances', + authMiddleware, + handleResponse(userController.checkUserBalances), +); router.get('/', authMiddleware, handleResponse(userController.find)); router.get( '/:id', diff --git a/packages/api/src/modules/user/service.ts b/packages/api/src/modules/user/service.ts index cf1d8d4ed..cd6316a8d 100644 --- a/packages/api/src/modules/user/service.ts +++ b/packages/api/src/modules/user/service.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { Brackets } from 'typeorm'; -import { User } from '@src/models'; +import { Predicate, User } from '@src/models'; import { PermissionRoles, Workspace, @@ -27,14 +27,23 @@ import { import App from '@src/server/app'; import { Address, Network } from 'fuels'; -import { Vault } from 'bakosafe'; import { PredicateService } from '../predicate/services'; import { Maybe } from '@src/utils/types/maybe'; -import { FuelProvider } from '@src/utils'; +import { FuelProvider, processBatch } from '@src/utils'; +import { BalanceCache } from '@src/server/storage/balance'; +import { TransactionCache } from '@src/server/storage/transaction'; +import { compareBalances } from '@src/utils/balance'; +import { emitBalanceOutdatedUser } from '@src/socket/events'; +import { SocketUsernames, SocketEvents } from '@src/socket/types'; +import { ProviderWithCache } from '@src/utils/ProviderWithCache'; +import { logger } from '@src/config/logger'; const { UI_URL } = process.env; +const MAX_PREDICATES_TO_CHECK_BALANCE = 50; +const PREDICATES_BALANCE_CHECK_BATCH_SIZE = 10; + export class UserService implements IUserService { private _pagination: PaginationParams; private _filter: IFilterParams; @@ -123,47 +132,38 @@ export class UserService implements IUserService { // insert a root wallet predicate const provider = await FuelProvider.create(payload.provider); - const configurable = { - SIGNATURES_COUNT: 1, - SIGNERS: [user.address], - network: provider.url, - chainId: provider.getChainId(), - }; - - // on creation, we dont need send the predicate version - const predicate = await new PredicateService().instancePredicate( - JSON.stringify(configurable), + + const vaults = await new PredicateService().checkOlderPredicateVersions( + user.address, + user.type, provider.url, ); - const network: Network = { - url: provider.url, - chainId: await provider.getChainId(), - }; - - await new PredicateService().create( - { - name: 'Personal Vault', - description: - 'This is your first vault. It requires a single signer (you) to execute transactions; a pattern called 1-of-1', - predicateAddress: Address.fromString( - predicate.address.toString(), - ).toB256(), - configurable: JSON.stringify(predicate.configurable), + for (const [i, vault] of vaults.entries()) { + const hasMultipleVaults = vaults.length > 1; + const isFirst = i === 0; + await Predicate.create({ + name: hasMultipleVaults ? `Predicate ${i + 1}` : 'Personal Account', + description: `${ + isFirst + ? 'This is your first account. It requires a single signer (you) to execute transactions; a pattern called 1-of-1' + : '' + }`, + predicateAddress: new Address(vault.address).toB256(), + configurable: JSON.stringify(vault.configurable), + root: isFirst, + version: vault.version, owner: user, - version: predicate.version, - members: [user], workspace, - root: true, - }, - network, - user, - workspace, - ); + members: [user], + }).save(); + } + await user.save(); return user; }) .catch(error => { + logger.error({ error }, 'Error on user create'); if (error instanceof GeneralError) throw error; throw new Internal({ @@ -325,4 +325,120 @@ export class UserService implements IUserService { return Pagination.create(queryBuilder).paginate(this._pagination); } + + async checkBalances({ + userId, + workspaceId, + network, + }: { + userId: string; + workspaceId: string; + network: Network; + }): Promise { + try { + // Get all predicates for this user (owner or member) + const predicates = await Predicate.createQueryBuilder('p') + .leftJoinAndSelect('p.members', 'members') + .leftJoinAndSelect('p.workspace', 'predicateWorkspace') + .select([ + 'p.id', + 'p.predicateAddress', + 'p.configurable', + 'p.version', + 'members.id', + 'predicateWorkspace.id', + ]) + .where('predicateWorkspace.id = :workspaceId', { + workspaceId, + }) + .andWhere('(p.owner_id = :userId OR members.id = :userId)', { + userId, + }) + .limit(MAX_PREDICATES_TO_CHECK_BALANCE) + .getMany(); + + const balanceCache = BalanceCache.getInstance(); + const transactionCache = TransactionCache.getInstance(); + const predicateService = new PredicateService(); + const outdatedPredicateIds: string[] = []; + + // Process predicates in batches to control concurrency + await processBatch( + predicates, + PREDICATES_BALANCE_CHECK_BATCH_SIZE, + async predicate => { + try { + const instance = await predicateService.instancePredicate( + predicate.configurable, + network.url, + predicate.version, + ); + + if (!(instance.provider instanceof ProviderWithCache)) { + return; + } + + // Get cached balance + const cachedBalances = await balanceCache.get( + predicate.predicateAddress, + network.chainId, + ); + + // Get current balance directly from blockchain (bypass cache) + const currentBalances = ( + await (instance.provider as ProviderWithCache).getBalancesFromBlockchain( + predicate.predicateAddress, + ) + ).balances.filter(a => a.amount.gt(0)); + + if (cachedBalances) { + const _cachedBalances = cachedBalances.filter(a => a.amount.gt(0)); + + const hasChanged = compareBalances(_cachedBalances, currentBalances); + + if (hasChanged) { + outdatedPredicateIds.push(predicate.id); + + // Update cache with fresh data + await balanceCache.set( + predicate.predicateAddress, + currentBalances, + network.chainId, + network.url, + ); + + // Invalidate transaction cache + await transactionCache.invalidate( + predicate.predicateAddress, + network.chainId, + ); + } + } + } catch (e) { + logger.error( + { predicateId: predicate.id, error: e?.message || e }, + '[CHECK_USER_BALANCES] Error checking predicate', + ); + } + }, + ); + + // Emit event to notify balance change only if there are outdated predicates + if (outdatedPredicateIds.length > 0) { + emitBalanceOutdatedUser(userId, { + sessionId: userId, + to: SocketUsernames.UI, + type: SocketEvents.BALANCE_OUTDATED_USER, + workspaceId, + outdatedPredicateIds, + }); + } + } catch (error) { + throw new Internal({ + type: ErrorTypes.Internal, + title: 'Error on check user balances', + detail: error?.message || error, + }); + } + } } diff --git a/packages/api/src/modules/user/types.ts b/packages/api/src/modules/user/types.ts index fd24e0dc0..2d1c8bda1 100644 --- a/packages/api/src/modules/user/types.ts +++ b/packages/api/src/modules/user/types.ts @@ -1,10 +1,17 @@ import { ContainerTypes, ValidatedRequestSchema } from 'express-joi-validation'; import { AuthValidatedRequest, UnloggedRequest } from '@src/middlewares/auth/types'; -import { UserSettings, TransactionType, TypeUser, User } from '@src/models'; +import { + TransactionStatus, + TransactionType, + User, + UserSettings, +} from '@src/models'; import { IDefaultOrdination, IOrdination } from '@src/utils/ordination'; import { IPagination, PaginationParams } from '@src/utils/pagination'; import { Maybe } from '@src/utils/types/maybe'; +import { TypeUser } from 'bakosafe'; +import { Network } from 'fuels'; export interface IWebAuthnSignUp { id: string; @@ -54,7 +61,17 @@ interface IListRequestSchema extends ValidatedRequestSchema { perPage: string; sort: 'ASC' | 'DESC'; orderBy: 'name' | IDefaultOrdination; - type: TransactionType; + }; +} + +interface IListUserTransactionsRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Query]: { + type?: TransactionType; + page?: string; + perPage?: string; + offsetDb?: string; + offsetFuel?: string; + status?: TransactionStatus[]; }; } @@ -112,6 +129,18 @@ export type ICheckHardwareRequest = UnloggedRequest; export type IMeInfoRequest = AuthValidatedRequest; +export type IListUserTransactionsRequest = AuthValidatedRequest; + +interface IAllocationRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Query]: { + limit?: number; + }; +} + +export type IAllocationRequest = AuthValidatedRequest; + +export type ICheckUserBalancesRequest = AuthValidatedRequest; + export interface IUserService { filter(filter: IFilterParams): this; paginate(pagination: PaginationParams): this; @@ -130,4 +159,9 @@ export interface IUserService { userId?: string, ): Promise>; listAll(): Promise>; + checkBalances: (params: { + userId: string; + workspaceId: string; + network: Network; + }) => Promise; } diff --git a/packages/api/src/modules/user/validation.ts b/packages/api/src/modules/user/validation.ts index 302148822..a27cad28d 100644 --- a/packages/api/src/modules/user/validation.ts +++ b/packages/api/src/modules/user/validation.ts @@ -1,7 +1,10 @@ +import { TransactionStatus, TransactionType } from 'bakosafe'; import Joi from 'joi'; import { AddressValidator, validator } from '@utils/index'; -import { Address } from 'fuels'; + +const allowedStatus = Object.values(TransactionStatus); +const allowedTypes = Object.values(TransactionType); export const PayloadCreateUserSchema = validator.body( Joi.object({ @@ -30,3 +33,23 @@ export const FindUserByIDParams = validator.params( id: Joi.string().uuid(), }), ); + +export const ListUserTransactionsQuerySchema = validator.query( + Joi.object({ + offsetDb: Joi.string().optional().default('0'), + offsetFuel: Joi.string().optional().default('0'), + perPage: Joi.string().optional().default('5'), + type: Joi.string() + .optional() + .valid(...allowedTypes), + status: Joi.array() + .items(Joi.string().valid(...allowedStatus)) + .optional(), + }), +); + +export const AllocationQuerySchema = validator.query( + Joi.object({ + limit: Joi.number().integer().min(1).max(50).optional().default(5), + }), +); diff --git a/packages/api/src/modules/webhook/controller.ts b/packages/api/src/modules/webhook/controller.ts new file mode 100644 index 000000000..37141b47a --- /dev/null +++ b/packages/api/src/modules/webhook/controller.ts @@ -0,0 +1,41 @@ +import { bindMethods, successful } from '@src/utils'; +import { logger } from '@src/config/logger'; +import { error } from '@src/utils/error'; +import { Webhook } from 'svix'; +import { + ILayersSwapWebhookRequest, + IMeldWebhookRequest, + IWebhookService, +} from './types'; + +export default class WebhookController { + constructor(private _service: IWebhookService) { + bindMethods(this); + } + + async handleMeldCryptoWebhook(request: IMeldWebhookRequest) { + try { + await this._service.handleMeldCryptoWebhook(request.body); + return successful({ message: 'Webhook processed successfully' }, 200); + } catch (e) { + logger.error({ error: e }, 'Error processing Meld Crypto webhook'); + return error(e.error, e.statusCode); + } + } + + async handleLayersSwapWebhook(req: ILayersSwapWebhookRequest) { + const payload: Buffer = req.body; + const headers = req.headers; + const { LAYERS_SWAP_WEBHOOK_SECRET } = process.env; + + const wh = new Webhook(LAYERS_SWAP_WEBHOOK_SECRET); + try { + const msg = wh.verify(payload, headers); + logger.info({ data: msg }, 'Webhook verified'); + return successful({ message: 'Webhook processed successfully' }, 200); + } catch (e) { + logger.error({ error: e }, 'Error verifying webhook'); + return error(e?.message || 'Invalid webhook signature', 400); + } + } +} diff --git a/packages/api/src/modules/webhook/routes.ts b/packages/api/src/modules/webhook/routes.ts new file mode 100644 index 000000000..615c4512a --- /dev/null +++ b/packages/api/src/modules/webhook/routes.ts @@ -0,0 +1,26 @@ +import { MeldAuthMiddleware } from '@src/middlewares/meld'; +import { handleResponse } from '@src/utils'; +import { Router } from 'express'; +import WebhookController from './controller'; +import WebhookService from './services'; +import bodyParser from 'body-parser'; + +const service = new WebhookService(); +const controller = new WebhookController(service); + +const webhookRouters = Router(); +const webhookRawRouters = Router(); + +webhookRouters.post( + '/meld/crypto', + MeldAuthMiddleware, + handleResponse(controller.handleMeldCryptoWebhook), +); + +webhookRawRouters.post( + '/bridge', + bodyParser.raw({ type: 'application/json' }), + handleResponse(controller.handleLayersSwapWebhook), +); + +export { webhookRouters, webhookRawRouters }; diff --git a/packages/api/src/modules/webhook/services.ts b/packages/api/src/modules/webhook/services.ts new file mode 100644 index 000000000..42f461c1f --- /dev/null +++ b/packages/api/src/modules/webhook/services.ts @@ -0,0 +1,215 @@ +import { networksByChainId } from '@src/constants/networks'; +import { + Predicate, + Transaction, + TransactionStatus, + TransactionTypeWithRamp, +} from '@src/models'; +import { RampTransaction } from '@src/models/RampTransactions'; +import { emitTransaction } from '@src/socket/events'; +import { SocketEvents, SocketUsernames } from '@src/socket/types'; +import { FuelProvider } from '@src/utils'; +import { ErrorTypes, Internal, NotFound } from '@src/utils/error'; +import { getTransactionSummary } from 'fuels'; +import { IMeldTransactionCryptoWeebhook } from '../meld/types'; +import { MeldApi, MeldApiFactory, MOCK_DEPOSIT_TX_ID } from '../meld/utils'; +import { TransactionController } from '../transaction/controller'; +import { + ICreateTransactionPayload, + ITransactionHistory, +} from '../transaction/types'; +import { getTransactionStatusByPaymentStatus } from './utils'; + +export default class WebhookService { + async handleMeldCryptoWebhook(data: IMeldTransactionCryptoWeebhook) { + const externalSessionId = data.payload.externalSessionId; + + if (externalSessionId) { + const meldData = await RampTransaction.createQueryBuilder('ramp') + .where( + `ramp.provider_data::jsonb -> 'widgetSessionData' ->> 'externalSessionId' = :sessionId`, + { sessionId: externalSessionId }, + ) + .leftJoin('ramp.transaction', 'transaction') + .leftJoin('ramp.user', 'user') + .addSelect(['transaction.id', 'user.id', 'transaction.status']) + .getOne(); + + if (!meldData) { + throw new NotFound({ + title: 'Meld transaction not found', + detail: `Transaction with external session ID ${externalSessionId} not found.`, + type: ErrorTypes.NotFound, + }); + } + const isSandbox = meldData.isSandbox; + const meldEnviroment = MeldApiFactory.getMeldEnvironment( + isSandbox ? 'sandbox' : 'production', + ); + const meldApi = new MeldApi(meldEnviroment.baseUrl, meldEnviroment.apiKey); + const meldTransactions = await meldApi.getMeldTransactions({ + externalSessionIds: externalSessionId, + }); + const blockchainTransactionId = isSandbox + ? MOCK_DEPOSIT_TX_ID + : meldTransactions?.transactions?.[0]?.cryptoDetails + ?.blockchainTransactionId; + + if (!meldData.transaction && blockchainTransactionId) { + const destinationAddress = meldData.userWalletAddress; + const chainId = meldTransactions?.transactions?.[0]?.cryptoDetails?.chainId; + const networkUrl = isSandbox + ? networksByChainId[0] + : networksByChainId[chainId] || networksByChainId['9889']; + const provider = await FuelProvider.create(networkUrl); + const txSummary = await getTransactionSummary({ + provider, + id: blockchainTransactionId, + }); + const predicate = await Predicate.findOneOrFail({ + where: { predicateAddress: destinationAddress }, + relations: { members: true }, + }); + const meldEthValue = isSandbox ? 'ETH' : 'ETH_FUEL'; + + const config = JSON.parse(predicate.configurable); + const meldTxData = meldTransactions.transactions[0]; + const isOnRamp = meldTxData.destinationCurrencyCode === meldEthValue; + + const newTransaction: ICreateTransactionPayload = { + type: isOnRamp + ? TransactionTypeWithRamp.ON_RAMP_DEPOSIT + : TransactionTypeWithRamp.OFF_RAMP_WITHDRAW, + status: getTransactionStatusByPaymentStatus( + data.payload.paymentTransactionStatus, + ), + gasUsed: txSummary.gasUsed.format(), + createdBy: meldData.user, + hash: txSummary.id.slice(2), + name: isOnRamp ? 'On Ramp' : 'Off Ramp', + resume: { + hash: txSummary.id, + status: TransactionStatus.SUCCESS, + witnesses: [], + requiredSigners: config.SIGNATURES_COUNT ?? 1, + totalSigners: predicate.members.length, + predicate: { + id: predicate.id, + address: predicate.predicateAddress, + }, + id: txSummary.id, + }, + txData: { + gasPrice: txSummary.transaction.gasPrice, + scriptGasLimit: txSummary.transaction.scriptGasLimit, + // @ts-expect-error - is script + script: txSummary.transaction.script, + // @ts-expect-error - is scriptData + scriptData: txSummary.transaction.scriptData, + // @ts-expect-error - is type + type: txSummary.transaction.type, + // @ts-expect-error - is witnesses + witnesses: txSummary.transaction.witnesses, + outputs: txSummary.transaction.outputs, + // @ts-expect-error - is inputs + inputs: txSummary.transaction.inputs, + }, + predicate, + // @ts-expect-error - no summary.type for this transaction + summary: { operations: txSummary.operations }, + network: { chainId: Number(chainId), url: networkUrl }, + }; + + const transaction = await Transaction.create(newTransaction) + .save() + .then(res => res) + .catch(err => { + throw new Internal({ + title: 'Error creating transaction', + detail: err instanceof Error ? err.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + }); + + const transactionHistory = await TransactionController.formatTransactionsHistory( + transaction, + ); + + emitTransaction(meldData.user.id, { + transaction: Transaction.formatTransactionResponse({ + ...transaction, + rampTransaction: meldData, + } as Transaction), + to: SocketUsernames.UI, + history: transactionHistory as ITransactionHistory[], + sessionId: meldData.user.id, + type: SocketEvents.TRANSACTION_CREATED, + }); + + await RampTransaction.update(meldData.id, { + providerData: { + ...meldData.providerData, + transactionData: meldTxData, + paymentStatus: data.payload.paymentTransactionStatus, + }, + destinationAmount: meldTxData.destinationAmount.toString(), + transaction, + }).catch(error => { + throw new Internal({ + title: 'Error updating Ramp transaction', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + }); + return; + } + + const newTxStatus = getTransactionStatusByPaymentStatus( + data.payload.paymentTransactionStatus, + ); + + await Transaction.update(meldData.transaction.id, { + status: + isSandbox && meldData?.transaction?.status === TransactionStatus.FAILED + ? TransactionStatus.FAILED + : newTxStatus, + }).catch(error => { + throw new Internal({ + title: 'Error updating transaction status', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + }); + + const updatedTx = await Transaction.findOneOrFail({ + where: { id: meldData.transaction.id }, + relations: { predicate: true, createdBy: true, rampTransaction: true }, + }); + + const transactionHistory = (await TransactionController.formatTransactionsHistory( + updatedTx, + )) as ITransactionHistory[]; + + emitTransaction(meldData.user.id, { + transaction: Transaction.formatTransactionResponse(updatedTx), + to: SocketUsernames.UI, + history: transactionHistory, + sessionId: meldData.user.id, + type: SocketEvents.TRANSACTION_UPDATED, + }); + + await RampTransaction.update(meldData.id, { + providerData: { + ...meldData.providerData, + paymentStatus: data.payload.paymentTransactionStatus, + }, + }).catch(error => { + throw new Internal({ + title: 'Error updating Ramp transaction', + detail: error instanceof Error ? error.message : 'Unknown error', + type: ErrorTypes.Internal, + }); + }); + } + } +} diff --git a/packages/api/src/modules/webhook/types.ts b/packages/api/src/modules/webhook/types.ts new file mode 100644 index 000000000..3e309e718 --- /dev/null +++ b/packages/api/src/modules/webhook/types.ts @@ -0,0 +1,21 @@ +import { + ContainerTypes, + ValidatedRequest, + ValidatedRequestSchema, +} from 'express-joi-validation'; +import { IMeldTransactionCryptoWeebhook } from '../meld/types'; + +export interface IWebhookService { + handleMeldCryptoWebhook(data: IMeldTransactionCryptoWeebhook): Promise; +} + +interface IMeldWebhookRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Body]: IMeldTransactionCryptoWeebhook; +} + +interface ILayersSwapWebhookRequestSchema extends ValidatedRequestSchema { + [ContainerTypes.Body]: Buffer; +} + +export type IMeldWebhookRequest = ValidatedRequest; +export type ILayersSwapWebhookRequest = ValidatedRequest; diff --git a/packages/api/src/modules/webhook/utils.ts b/packages/api/src/modules/webhook/utils.ts new file mode 100644 index 000000000..e3a73f172 --- /dev/null +++ b/packages/api/src/modules/webhook/utils.ts @@ -0,0 +1,31 @@ +import { TransactionStatusWithRamp } from '@src/models'; +import { ErrorTypes, Internal } from '@src/utils/error'; +import { TransactionStatus } from 'bakosafe'; +import { IMeldTransactionCryptoWeebhook } from '../meld/types'; + +type Status = IMeldTransactionCryptoWeebhook['payload']['paymentTransactionStatus']; + +export const getTransactionStatusByPaymentStatus = ( + status: Status, +): TransactionStatusWithRamp | TransactionStatus => { + switch (status) { + case 'PENDING_CREATED': + return TransactionStatusWithRamp.PENDING_PROVIDER; + case 'PENDING': + return TransactionStatusWithRamp.PENDING_PROVIDER; + case 'SETTLING': + return TransactionStatusWithRamp.PENDING_PROVIDER; + case 'SETTLED': + return TransactionStatus.SUCCESS; + case 'FAILED': + return TransactionStatus.FAILED; + case 'ERROR': + return TransactionStatus.FAILED; + default: + throw new Internal({ + title: 'Invalid payment transaction status', + detail: `Received an invalid payment transaction status: ${status}`, + type: ErrorTypes.Internal, + }); + } +}; diff --git a/packages/api/src/modules/workspace/controller.ts b/packages/api/src/modules/workspace/controller.ts index 0af513d9f..78cd01072 100644 --- a/packages/api/src/modules/workspace/controller.ts +++ b/packages/api/src/modules/workspace/controller.ts @@ -1,6 +1,6 @@ -import { TransactionStatus } from 'bakosafe'; +import { TypeUser } from 'bakosafe'; -import { Predicate, TypeUser, User, PermissionAccess } from '@src/models'; +import { User, PermissionAccess } from '@src/models'; import { PermissionRoles, Workspace } from '@src/models/Workspace'; import Internal from '@src/utils/error/Internal'; import { diff --git a/packages/api/src/modules/workspace/services.ts b/packages/api/src/modules/workspace/services.ts index 21dbbaade..f2e425e84 100644 --- a/packages/api/src/modules/workspace/services.ts +++ b/packages/api/src/modules/workspace/services.ts @@ -1,6 +1,6 @@ -// import { BakoSafe } from 'bakosafe'; +import { TypeUser } from 'bakosafe'; -import { TypeUser, User, PermissionAccess } from '@src/models'; +import { User, PermissionAccess } from '@src/models'; import { IPermissions, PermissionRoles, diff --git a/packages/api/src/routes.ts b/packages/api/src/routes.ts index 4f4f5c48a..bf67efe1d 100644 --- a/packages/api/src/routes.ts +++ b/packages/api/src/routes.ts @@ -3,13 +3,18 @@ import { Router } from 'express'; import users from '@src/modules/user/routes'; import addressBook from '@modules/addressBook/routes'; +import apiToken, { cliAuthRoute } from '@modules/apiToken/routes'; import auth from '@modules/auth/routes'; import dApp from '@modules/dApps/routes'; +import meld from '@modules/meld/routes'; import notifications from '@modules/notification/routes'; import predicates from '@modules/predicate/routes'; +import rampTransactions from '@modules/rampTransactions/routes'; +import bridge from '@src/modules/bridge/routes'; import transactions from '@modules/transaction/routes'; +import { webhookRouters } from './modules/webhook/routes'; +import { internalRouter } from './modules/internal/routes'; import workspace from '@modules/workspace/routes'; -import apiToken, { cliAuthRoute } from '@modules/apiToken/routes'; // import debugPprof from '@modules/debugPprof/routes'; import externalRoute from '@modules/external/routes'; import healtCheckRouter from '@modules/healthCheck/routes'; @@ -25,11 +30,15 @@ router.use('/connections', dApp); router.use('/api-token', apiToken); router.use('/workspace', workspace); router.use('/predicate', predicates); -// router.use('/debug-pprof', debugPprof); router.use('/address-book', addressBook); router.use('/transaction', transactions); router.use('/notifications', notifications); router.use('/external', externalRoute); +router.use('/ramp-transactions/meld', meld); +router.use('/ramp-transactions', rampTransactions); +router.use('/bridge', bridge); +router.use('/webhooks', webhookRouters); +router.use('/internal', internalRouter); router.use('/healthcheck', healtCheckRouter); // ping route diff --git a/packages/api/src/server/app.ts b/packages/api/src/server/app.ts index 02b81d55f..bc36600b4 100644 --- a/packages/api/src/server/app.ts +++ b/packages/api/src/server/app.ts @@ -1,4 +1,5 @@ import bodyParser from 'body-parser'; +import { logger } from '@src/config/logger'; import cookieParser from 'cookie-parser'; import cors from 'cors'; import Express from 'express'; @@ -8,11 +9,17 @@ import { router } from '@src/routes'; import { isDevMode } from '@src/utils'; import { handleErrors } from '@middlewares/index'; -import { QuoteStorage, SessionStorage } from './storage'; +import { + QuoteStorage, + SessionStorage, + BalanceCache, + TransactionCache, +} from './storage'; import Monitoring from './monitoring'; import Bootstrap from './bootstrap'; import { RedisWriteClient, RedisReadClient, FuelProvider } from '@src/utils'; import { RigInstance } from './storage/rig'; +import { webhookRawRouters } from '@src/modules/webhook/routes'; class App { private static instance?: App; @@ -20,10 +27,13 @@ class App { private readonly app: Express.Application; private sessionCache: SessionStorage; private quoteCache: QuoteStorage; - private rigCache: Promise; + private rigCache: Promise | null; + private balanceCache: BalanceCache; + private transactionCache: TransactionCache; protected constructor() { this.app = Express(); + this.app.use('/webhooks', webhookRawRouters); this.initMiddlewares(); this.initRoutes(); this.setupMonitoring(); @@ -35,7 +45,18 @@ class App { // } this.sessionCache = SessionStorage.start(); this.quoteCache = QuoteStorage.start(); - this.rigCache = RigInstance.start(); + this.balanceCache = BalanceCache.start(); + this.transactionCache = TransactionCache.start(); + + // RIG is optional - only start if contract address is configured + if (process.env.RIG_ID_CONTRACT) { + this.rigCache = RigInstance.start(); + } else { + this.rigCache = null; + logger.info( + '[APP] RIG_ID_CONTRACT not configured, skipping RIG initialization', + ); + } } private initMiddlewares() { @@ -78,6 +99,14 @@ class App { return this.rigCache; } + get _balanceCache() { + return this.balanceCache; + } + + get _transactionCache() { + return this.transactionCache; + } + static stop() { return Bootstrap.stop() .then(() => RedisWriteClient.stop()) @@ -85,12 +114,18 @@ class App { .then(() => FuelProvider.stop()) .then(() => SessionStorage.stop()) .then(() => QuoteStorage.stop()) - .then(() => RigInstance.stop()) + .then(() => { + if (process.env.RIG_ID_CONTRACT) { + return RigInstance.stop(); + } + }) + .then(() => BalanceCache.stop()) + .then(() => TransactionCache.stop()) .then(() => { App.instance = undefined; }) .catch(error => { - console.error('[APP] Error stopping application:', error); + logger.error({ error: error }, '[APP] Error stopping application'); }); } diff --git a/packages/api/src/server/index.ts b/packages/api/src/server/index.ts index fa4c7d412..202518989 100644 --- a/packages/api/src/server/index.ts +++ b/packages/api/src/server/index.ts @@ -2,16 +2,17 @@ import 'reflect-metadata'; // import * as pprof from 'pprof'; import './tracing'; import App from './app'; +import { logger } from '@src/config/logger'; const start = async () => { const app = await App.start(); const port = process.env.API_PORT || process.env.PORT || 3000; const API_ENVIRONMENT = process.env.API_ENVIRONMENT || 'development'; - console.log('[APP] Storages started'); + logger.info('[APP] Storages started'); app.serverApp.listen(port, () => { - console.log( + logger.info( `[APP] Application running in http://localhost:${port} mode ${API_ENVIRONMENT}`, ); }); @@ -20,5 +21,5 @@ const start = async () => { try { start(); } catch (e) { - console.log(e); + logger.error({ error: e }, '[APP] Error starting application'); } diff --git a/packages/api/src/server/log.ts b/packages/api/src/server/log.ts index 29ce8b20c..105455646 100644 --- a/packages/api/src/server/log.ts +++ b/packages/api/src/server/log.ts @@ -1,3 +1,5 @@ +import { logger } from '@src/config/logger'; + const { API_PORT, PORT, @@ -16,20 +18,23 @@ const { } = process.env; export const environment = async () => { - console.log('[ENVIRONMENTS]: ', { - API_PORT, - PORT, - API_ENVIRONMENT, - UI_URL, - API_URL, - FUEL_PROVIDER, - GAS_LIMIT, - MAX_FEE, - COIN_MARKET_CAP_API_KEY, - AWS_SMTP_USER, - AWS_SMTP_PASS, - REDIS_URL_WRITE, - REDIS_URL_READ, - EXTERN_TOKEN_SECRET, - }); + logger.info( + { + API_PORT, + PORT, + API_ENVIRONMENT, + UI_URL, + API_URL, + FUEL_PROVIDER, + GAS_LIMIT, + MAX_FEE, + COIN_MARKET_CAP_API_KEY, + AWS_SMTP_USER, + AWS_SMTP_PASS, + REDIS_URL_WRITE, + REDIS_URL_READ, + EXTERN_TOKEN_SECRET, + }, + '[ENVIRONMENTS]', + ); }; diff --git a/packages/api/src/server/storage/balance.ts b/packages/api/src/server/storage/balance.ts new file mode 100644 index 000000000..3ec6f9196 --- /dev/null +++ b/packages/api/src/server/storage/balance.ts @@ -0,0 +1,475 @@ +import { logger } from '@src/config/logger'; +import { RedisReadClient, RedisWriteClient } from '@src/utils'; +import { cacheConfig, CacheMetrics, CacheStats } from '@src/config/cache'; +import { bn, CoinQuantity } from 'fuels'; + +const { prefixes, ttl, invalidationFlagTtl } = cacheConfig; + +/** + * Serialized format for storing in Redis + * BigNumbers are stored as hex strings + */ +interface SerializedBalance { + assetId: string; + amount: string; // hex string +} + +interface CachedBalanceData { + balances: SerializedBalance[]; + timestamp: number; + chainId: number; + networkUrl?: string; +} + +export interface BalanceCacheStats extends CacheStats { + totalKeys: number; + byChain: Record; + config: { + enabled: boolean; + ttl: number; + invalidationFlagTtl: number; + }; +} + +/** + * BalanceCache - Redis-based cache for predicate balances + * + * Features: + * - Serializes/deserializes BigNumbers (BN) to hex strings + * - Supports invalidation by predicate address, chainId, userId, or workspaceId + * - Tracks cache metrics (hits, misses, invalidations) + * - Configurable TTL via environment variables + */ +export class BalanceCache { + private static instance?: BalanceCache; + + protected constructor() {} + + /** + * Build cache key for balance + */ + private buildKey(predicateAddress: string, chainId: number): string { + return `${prefixes.balance}:${predicateAddress}:${chainId}`; + } + + /** + * Build invalidation flag key + */ + private buildInvalidationKey(predicateAddress: string, chainId?: number): string { + return chainId + ? `${prefixes.invalidated}:${predicateAddress}:${chainId}` + : `${prefixes.invalidated}:${predicateAddress}`; + } + + /** + * Serialize balances for Redis storage + * Converts BN to hex strings + */ + private serializeBalances(balances: CoinQuantity[]): SerializedBalance[] { + return balances.map(b => ({ + assetId: b.assetId, + amount: b.amount.toHex(), + })); + } + + /** + * Deserialize balances from Redis + * Converts hex strings back to BN + */ + private deserializeBalances(serialized: SerializedBalance[]): CoinQuantity[] { + return serialized.map(s => ({ + assetId: s.assetId, + amount: bn(s.amount), + })); + } + + /** + * Check if there's an invalidation flag for this predicate + */ + async isInvalidated(predicateAddress: string, chainId: number): Promise { + // Check specific chainId flag + const specificFlag = await RedisReadClient.get( + this.buildInvalidationKey(predicateAddress, chainId), + ); + if (specificFlag) return true; + + // Check global flag (all chains) + const globalFlag = await RedisReadClient.get( + this.buildInvalidationKey(predicateAddress), + ); + return !!globalFlag; + } + + /** + * Check if cache exists for a predicate (without reading full data) + * Useful for warm-up to skip already cached predicates + */ + async exists(predicateAddress: string, chainId: number): Promise { + if (!cacheConfig.enabled) { + return false; + } + + try { + // Check if invalidated first + const invalidated = await this.isInvalidated(predicateAddress, chainId); + if (invalidated) { + return false; + } + + const key = this.buildKey(predicateAddress, chainId); + const exists = await RedisReadClient.exists(key); + return exists; + } catch { + return false; + } + } + + /** + * Check which addresses are NOT cached (for batch warm-up) + * Returns addresses that need to be fetched + */ + async filterUncached(addresses: string[], chainId: number): Promise { + if (!cacheConfig.enabled || addresses.length === 0) { + return addresses; + } + + try { + const results = await Promise.all( + addresses.map(async addr => ({ + address: addr, + cached: await this.exists(addr, chainId), + })), + ); + + return results.filter(r => !r.cached).map(r => r.address); + } catch { + return addresses; // On error, assume none are cached + } + } + + /** + * Get cached balances for a predicate + * Returns null if not cached, expired, or invalidated + */ + async get( + predicateAddress: string, + chainId: number, + ): Promise { + if (!cacheConfig.enabled) { + return null; + } + + try { + // Check if invalidated + const invalidated = await this.isInvalidated(predicateAddress, chainId); + if (invalidated) { + CacheMetrics.miss(); + return null; + } + + const key = this.buildKey(predicateAddress, chainId); + const cached = await RedisReadClient.get(key); + + if (!cached) { + CacheMetrics.miss(); + return null; + } + + const data: CachedBalanceData = JSON.parse(cached); + const balances = this.deserializeBalances(data.balances); + + CacheMetrics.hit(); + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + chainId, + time: Math.round((Date.now() - data.timestamp) / 1000), + }, + '[BalanceCache] HIT', + ); + + return balances; + } catch (error) { + logger.error({ error }, '[BalanceCache] GET'); + CacheMetrics.error(); + return null; + } + } + + /** + * Set cached balances for a predicate + */ + async set( + predicateAddress: string, + balances: CoinQuantity[], + chainId: number, + networkUrl?: string, + ): Promise { + if (!cacheConfig.enabled) { + return; + } + + try { + const key = this.buildKey(predicateAddress, chainId); + const data: CachedBalanceData = { + balances: this.serializeBalances(balances), + timestamp: Date.now(), + chainId, + networkUrl, + }; + + await RedisWriteClient.setWithTTL(key, JSON.stringify(data), ttl); + + // Clear any invalidation flags after setting new data + await this.clearInvalidationFlag(predicateAddress, chainId); + + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + chainId, + assetsCount: balances.length, + }, + '[BalanceCache] SET', + ); + } catch (error) { + logger.error({ error }, '[BalanceCache] SET'); + CacheMetrics.error(); + } + } + + /** + * Invalidate cache for a predicate + * If chainId is provided, only that chain is invalidated + * Otherwise, all chains for that predicate are invalidated + */ + async invalidate(predicateAddress: string, chainId?: number): Promise { + try { + if (chainId) { + // Invalidate specific chain + const key = this.buildKey(predicateAddress, chainId); + await RedisWriteClient.del([key]); + + // Set invalidation flag + await RedisWriteClient.setWithTTL( + this.buildInvalidationKey(predicateAddress, chainId), + String(Date.now()), + invalidationFlagTtl, + ); + + CacheMetrics.invalidate(); + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + chainId, + }, + '[BalanceCache] INVALIDATED', + ); + } else { + // Invalidate all chains for this predicate + const pattern = `${prefixes.balance}:${predicateAddress}:*`; + const deletedCount = await RedisWriteClient.delByPattern(pattern); + + // Set global invalidation flag + await RedisWriteClient.setWithTTL( + this.buildInvalidationKey(predicateAddress), + String(Date.now()), + invalidationFlagTtl, + ); + + CacheMetrics.invalidate(deletedCount || 1); + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + deletedCount, + }, + '[BalanceCache] INVALIDATED for all chains', + ); + } + } catch (error) { + logger.error({ error }, '[BalanceCache] INVALIDATE'); + CacheMetrics.error(); + } + } + + /** + * Invalidate cache for all predicates of a user + * Requires querying the database for user's predicates + */ + async invalidateByUser( + userId: string, + getPredicateAddresses: () => Promise, + ): Promise { + try { + const addresses = await getPredicateAddresses(); + let count = 0; + + for (const address of addresses) { + await this.invalidate(address); + count++; + } + + logger.info( + { + userId, + predicatesCount: count, + }, + '[BalanceCache] INVALIDATED by user', + ); + return count; + } catch (error) { + logger.error({ error }, '[BalanceCache] INVALIDATE_BY_USER'); + CacheMetrics.error(); + return 0; + } + } + + /** + * Invalidate all cached balances (use with caution!) + */ + async invalidateAll(): Promise { + try { + const pattern = `${prefixes.balance}:*`; + const deletedCount = await RedisWriteClient.delByPattern(pattern); + + // Also clear invalidation flags + const invalidationPattern = `${prefixes.invalidated}:*`; + await RedisWriteClient.delByPattern(invalidationPattern); + + CacheMetrics.invalidate(deletedCount); + logger.info( + { + deletedCount, + }, + '[BalanceCache] INVALIDATED ALL', + ); + + return deletedCount; + } catch (error) { + logger.error({ error }, '[BalanceCache] INVALIDATE_ALL'); + CacheMetrics.error(); + return 0; + } + } + + /** + * Clear invalidation flag for a predicate + */ + async clearInvalidationFlag( + predicateAddress: string, + chainId?: number, + ): Promise { + try { + const keys = [this.buildInvalidationKey(predicateAddress)]; + if (chainId) { + keys.push(this.buildInvalidationKey(predicateAddress, chainId)); + } + await RedisWriteClient.del(keys); + } catch (error) { + logger.error({ error }, '[BalanceCache] CLEAR_INVALIDATION'); + } + } + + /** + * Clear specific cache entry + */ + async clear(predicateAddress: string, chainId?: number): Promise { + try { + if (chainId) { + const key = this.buildKey(predicateAddress, chainId); + await RedisWriteClient.del([key]); + } else { + const pattern = `${prefixes.balance}:${predicateAddress}:*`; + await RedisWriteClient.delByPattern(pattern); + } + } catch (error) { + logger.error({ error }, '[BalanceCache] CLEAR'); + } + } + + /** + * Get cache statistics + */ + async stats(): Promise { + const metrics = await CacheMetrics.getStats(); + + try { + // Get all balance keys + const keys = await RedisReadClient.keys(`${prefixes.balance}:*`); + + // Count by chainId + const byChain: Record = {}; + for (const key of keys) { + const parts = key.split(':'); + const chainId = parseInt(parts[parts.length - 1], 10); + if (!isNaN(chainId)) { + byChain[chainId] = (byChain[chainId] || 0) + 1; + } + } + + return { + ...metrics, + totalKeys: keys.length, + byChain, + config: { + enabled: cacheConfig.enabled, + ttl, + invalidationFlagTtl, + }, + }; + } catch (error) { + logger.error({ error }, '[BalanceCache] STATS'); + return { + ...metrics, + totalKeys: 0, + byChain: {}, + config: { + enabled: cacheConfig.enabled, + ttl, + invalidationFlagTtl, + }, + }; + } + } + + /** + * Get all cache keys (for debugging) + */ + async keys(pattern?: string): Promise { + const searchPattern = pattern || `${prefixes.balance}:*`; + return RedisReadClient.keys(searchPattern); + } + + /** + * Start the BalanceCache singleton + */ + static start(): BalanceCache { + if (!BalanceCache.instance) { + BalanceCache.instance = new BalanceCache(); + logger.info( + { enabled: cacheConfig.enabled, ttl: `${ttl}s` }, + '[BalanceCache] Started', + ); + } + return BalanceCache.instance; + } + + /** + * Stop the BalanceCache + */ + static stop(): void { + if (BalanceCache.instance) { + BalanceCache.instance = undefined; + logger.info('[BalanceCache] Stopped'); + } + } + + /** + * Get the singleton instance + */ + static getInstance(): BalanceCache { + if (!BalanceCache.instance) { + throw new Error('BalanceCache not started'); + } + return BalanceCache.instance; + } +} diff --git a/packages/api/src/server/storage/fuelAssetsFetcher.ts b/packages/api/src/server/storage/fuelAssetsFetcher.ts index e38ebe449..8a1d9e22e 100644 --- a/packages/api/src/server/storage/fuelAssetsFetcher.ts +++ b/packages/api/src/server/storage/fuelAssetsFetcher.ts @@ -1,4 +1,5 @@ import { assets, Assets } from 'fuels'; +import { logger } from '@src/config/logger'; const ASSETS_URL = 'https://verified-assets.fuel.network/assets.json'; @@ -21,7 +22,7 @@ export const fetchFuelAssets = async (): Promise => { return cachedAssets; } catch (error) { - console.error('Error fetching fuel assets:', error); + logger.error({ error }, 'Error fetching fuel assets'); return [] as Assets; } }; diff --git a/packages/api/src/server/storage/index.ts b/packages/api/src/server/storage/index.ts index 50e733aa3..aad2c3fe8 100644 --- a/packages/api/src/server/storage/index.ts +++ b/packages/api/src/server/storage/index.ts @@ -1,2 +1,4 @@ export * from './quote'; export * from './session'; +export * from './balance'; +export * from './transaction'; diff --git a/packages/api/src/server/storage/quote.ts b/packages/api/src/server/storage/quote.ts index 120eaf82b..3b5a5a6b1 100644 --- a/packages/api/src/server/storage/quote.ts +++ b/packages/api/src/server/storage/quote.ts @@ -7,6 +7,7 @@ import { isDevMode, } from '@src/utils'; import { tokensIDS } from '@src/utils/assets-token/addresses'; +import { logger } from '@src/config/logger'; import axios from 'axios'; import App from '../app'; @@ -42,42 +43,73 @@ export class QuoteStorage { await this.setQuotes(QuotesMock); } - private async getStFUELQuote(QuotesMock: IQuote[]): Promise { + /** + * Calcula o preço do stFUEL baseado no preço do FUEL e no ratio do Rig + * stFUEL é um token de staking líquido que representa FUEL em stake + * Formula: preço_stFUEL = preço_FUEL * ratio + * Ex: Se FUEL = $2 e ratio = 1.05, então stFUEL = $2 * 1.05 = $2.10 + */ + private async calculateStFUELPrice(quotes: IQuote[]): Promise { + const rigCache = App.getInstance()._rigCache; + if (!rigCache) { + // RIG not configured, skip stFUEL quote calculation + return 0; + } + const DECIMALS = 10 ** 9; - const priceFUEL = QuotesMock.find(quote => quote.assetId === tokensIDS.FUEL) - .price; - const rigInstance = await App.getInstance()._rigCache; - const ratioStFuelToFuel = (await rigInstance.getRatio()) / DECIMALS; - return priceFUEL / ratioStFuelToFuel; + const fuelQuote = quotes.find(q => q.assetId === tokensIDS.FUEL); + + if (!fuelQuote) { + logger.warn('FUEL quote not found, cannot calculate stFUEL price'); + return 0; + } + + try { + const rigInstance = await rigCache; + const ratio = (await rigInstance.getRatio()) / DECIMALS; + // stFUEL vale MAIS que FUEL devido ao acúmulo de recompensas + return fuelQuote.price * ratio; + } catch (error) { + logger.error({ error }, 'Error calculating stFUEL price:'); + return 0; + } } - private updateStFUELQuote(quotes: IQuote[], priceStFuel: number) { - const stFuelIndex = quotes.findIndex(q => q.assetId === tokensIDS.stFUEL); - if (stFuelIndex >= 0) quotes[stFuelIndex].price = priceStFuel; + /** + * Adiciona tokens derivados (como stFUEL) aos quotes base + * Facilita adicionar outros tokens derivados no futuro (ex: wstETH, rETH) + */ + private async enrichWithDerivedTokens(quotes: IQuote[]): Promise { + const enriched = [...quotes]; + + // Adiciona stFUEL calculado dinamicamente + const stFuelPrice = await this.calculateStFUELPrice(quotes); + enriched.push({ + assetId: tokensIDS.stFUEL, + price: stFuelPrice, + }); + + return enriched; } private async addQuotes(): Promise { const { assets, assetsMapById, QuotesMock } = await getAssetsMaps(); - if (isDevMode) { - const priceStFuel = await this.getStFUELQuote(QuotesMock); - this.updateStFUELQuote(QuotesMock, priceStFuel); + let baseQuotes: IQuote[]; - await this.addMockQuotes(QuotesMock); - return; + if (isDevMode) { + baseQuotes = QuotesMock; + } else { + const _assets = this.generateAssets(assets); + const params = this.generateParams(assetsMapById, assets); + baseQuotes = params ? await this.fetchQuotes(_assets, params) : []; } - const _assets = this.generateAssets(assets); - const params = this.generateParams(assetsMapById, assets); + // Adiciona tokens derivados (stFUEL, etc) + const allQuotes = await this.enrichWithDerivedTokens(baseQuotes); - if (params) { - const quotes = await this.fetchQuotes(_assets, params); - - const priceStFuel = await this.getStFUELQuote(quotes); - this.updateStFUELQuote(quotes, priceStFuel); - - await this.setQuotes(quotes); - } + // Salva todos os quotes no Redis + await this.setQuotes(allQuotes); } private generateAssets(assets: IAsset[]) { diff --git a/packages/api/src/server/storage/rig.ts b/packages/api/src/server/storage/rig.ts index ffb3347d1..1edaa624e 100644 --- a/packages/api/src/server/storage/rig.ts +++ b/packages/api/src/server/storage/rig.ts @@ -1,8 +1,10 @@ import { Rig } from '@src/contracts/rig/mainnet/types'; -import { networks } from '@src/tests/mocks/Networks'; +import { networksByChainId } from '@src/constants/networks'; import { Vault } from 'bakosafe'; import { Provider } from 'fuels'; + const { RIG_ID_CONTRACT } = process.env; +const MAINNET_CHAIN_ID = '9889'; export class RigInstance { private static instance?: RigInstance; @@ -20,7 +22,7 @@ export class RigInstance { static async start(): Promise { if (!RigInstance.instance) { - const provider = new Provider(networks['MAINNET']); + const provider = new Provider(networksByChainId[MAINNET_CHAIN_ID]); const version = ''; const vault = new Vault( diff --git a/packages/api/src/server/storage/transaction.ts b/packages/api/src/server/storage/transaction.ts new file mode 100644 index 000000000..5559be927 --- /dev/null +++ b/packages/api/src/server/storage/transaction.ts @@ -0,0 +1,471 @@ +import { RedisReadClient, RedisWriteClient } from '@src/utils'; +import { logger } from '@src/config/logger'; +import { cacheConfig, CacheMetrics } from '@src/config/cache'; + +const { prefixes } = cacheConfig; + +// Transaction cache specific config +// Longer TTL since we only cache confirmed transactions (deposits) +// which are immutable once on-chain +const TRANSACTION_CACHE_TTL = parseInt( + process.env.TRANSACTION_CACHE_TTL || '600', + 10, +); // 10 minutes default (confirmed txs are immutable) +const TRANSACTION_CACHE_PREFIX = 'tx'; + +// How many recent transactions to fetch on incremental refresh +const INCREMENTAL_FETCH_LIMIT = parseInt( + process.env.TRANSACTION_INCREMENTAL_LIMIT || '10', + 10, +); + +/** + * Generic transaction type for cache + * Uses unknown[] to be compatible with any transaction type + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type CacheableTransaction = Record; + +/** + * Serialized transaction for Redis storage + * We store the formatted transaction response directly + */ +interface CachedTransactionData { + transactions: CacheableTransaction[]; + timestamp: number; + chainId: number; + predicateAddress: string; + // Store known transaction hashes for fast deduplication + knownHashes: string[]; +} + +/** + * Result from incremental fetch + */ +export interface IncrementalFetchResult { + cachedTransactions: CacheableTransaction[]; + needsIncrementalFetch: boolean; + knownHashes: Set; +} + +/** + * TransactionCache - Redis-based cache for Fuel blockchain transactions + * + * Features: + * - Caches confirmed transactions by predicate address and chainId + * - Long TTL (10 minutes) since confirmed txs are immutable + * - Incremental refresh: on invalidation, fetches only new txs and merges + * - Deduplication using transaction hashes + */ +export class TransactionCache { + private static instance?: TransactionCache; + + protected constructor() {} + + /** + * Build cache key for transactions + */ + private buildKey(predicateAddress: string, chainId: number): string { + return `${TRANSACTION_CACHE_PREFIX}:${predicateAddress}:${chainId}`; + } + + /** + * Build refresh flag key (indicates cache needs incremental update) + */ + private buildRefreshKey(predicateAddress: string, chainId: number): string { + return `${TRANSACTION_CACHE_PREFIX}:refresh:${predicateAddress}:${chainId}`; + } + + /** + * Check if cache needs incremental refresh + */ + async needsRefresh(predicateAddress: string, chainId: number): Promise { + const refreshFlag = await RedisReadClient.get( + this.buildRefreshKey(predicateAddress, chainId), + ); + return !!refreshFlag; + } + + /** + * Get cached transactions and check if incremental fetch is needed + * Returns cached data + flag indicating if caller should fetch new txs + */ + async getWithRefreshCheck( + predicateAddress: string, + chainId: number, + ): Promise { + if (!cacheConfig.enabled) { + return { + cachedTransactions: [], + needsIncrementalFetch: true, + knownHashes: new Set(), + }; + } + + try { + const key = this.buildKey(predicateAddress, chainId); + const cached = await RedisReadClient.get(key); + const needsRefresh = await this.needsRefresh(predicateAddress, chainId); + + if (!cached) { + CacheMetrics.miss(); + return { + cachedTransactions: [], + needsIncrementalFetch: true, + knownHashes: new Set(), + }; + } + + const data: CachedTransactionData = JSON.parse(cached); + const knownHashes = new Set(data.knownHashes || []); + + if (needsRefresh) { + // Cache exists but needs incremental update + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + chainId, + cachedTxCount: data.transactions.length, + }, + '[TxCache] REFRESH needed for predicate', + ); + return { + cachedTransactions: data.transactions, + needsIncrementalFetch: true, + knownHashes, + }; + } + + // Cache is fresh + CacheMetrics.hit(); + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + chainId, + ageSeconds: Math.round((Date.now() - data.timestamp) / 1000), + transactionCount: data.transactions.length, + }, + '[TxCache] HIT', + ); + + return { + cachedTransactions: data.transactions, + needsIncrementalFetch: false, + knownHashes, + }; + } catch (error) { + logger.error({ error }, '[TxCache] GET'); + CacheMetrics.error(); + return { + cachedTransactions: [], + needsIncrementalFetch: true, + knownHashes: new Set(), + }; + } + } + + /** + * Legacy get method for backwards compatibility + * Returns null if not cached or needs refresh + */ + async get( + predicateAddress: string, + chainId: number, + ): Promise { + const result = await this.getWithRefreshCheck(predicateAddress, chainId); + + if (result.needsIncrementalFetch && result.cachedTransactions.length === 0) { + return null; + } + + if (result.needsIncrementalFetch) { + // Has cache but needs refresh - return null to trigger full fetch + // The caller should use getWithRefreshCheck for incremental behavior + return null; + } + + return result.cachedTransactions; + } + + /** + * Set cached transactions for a predicate + */ + async set( + predicateAddress: string, + transactions: CacheableTransaction[], + chainId: number, + ): Promise { + if (!cacheConfig.enabled) { + return; + } + + try { + const key = this.buildKey(predicateAddress, chainId); + + // Extract hashes for deduplication + const knownHashes = transactions + .map(tx => tx.hash || tx.id) + .filter((h): h is string => !!h); + + const data: CachedTransactionData = { + transactions, + timestamp: Date.now(), + chainId, + predicateAddress, + knownHashes, + }; + + await RedisWriteClient.setWithTTL( + key, + JSON.stringify(data), + TRANSACTION_CACHE_TTL, + ); + + // Clear refresh flag after setting new data + await this.clearRefreshFlag(predicateAddress, chainId); + + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + chainId, + transactionCount: transactions.length, + ttl: `${TRANSACTION_CACHE_TTL}s`, + }, + '[TxCache] SET', + ); + } catch (error) { + logger.error({ error }, '[TxCache] SET'); + CacheMetrics.error(); + } + } + + /** + * Merge new transactions with cached ones (deduplication by hash) + */ + mergeTransactions( + cached: T[], + newTxs: T[], + knownHashes: Set, + ): T[] { + // Filter out duplicates from new transactions + const uniqueNewTxs = newTxs.filter(tx => { + const hash = tx.hash || tx.id; + return hash && !knownHashes.has(hash); + }); + + if (uniqueNewTxs.length > 0) { + logger.info( + { + newTransactionCount: uniqueNewTxs.length, + cachedTransactionCount: cached.length, + }, + '[TxCache] MERGE', + ); + } + + // Combine and sort by date (newest first) + const merged = [...uniqueNewTxs, ...cached]; + return merged.sort((a, b) => { + const dateA = new Date(a.createdAt || 0).getTime(); + const dateB = new Date(b.createdAt || 0).getTime(); + return dateB - dateA; + }); + } + + /** + * Mark cache as needing refresh (instead of deleting) + * Called when a new transaction is created or confirmed + */ + async markForRefresh(predicateAddress: string, chainId?: number): Promise { + try { + if (chainId) { + await RedisWriteClient.setWithTTL( + this.buildRefreshKey(predicateAddress, chainId), + String(Date.now()), + TRANSACTION_CACHE_TTL, // Flag expires with cache + ); + + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + chainId, + }, + '[TxCache] MARKED for refresh', + ); + } else { + // Mark all chains for this predicate + const keys = await RedisReadClient.keys( + `${TRANSACTION_CACHE_PREFIX}:${predicateAddress}:*`, + ); + + for (const key of keys) { + const parts = key.split(':'); + const keyChainId = parseInt(parts[parts.length - 1], 10); + if (!isNaN(keyChainId)) { + await RedisWriteClient.setWithTTL( + this.buildRefreshKey(predicateAddress, keyChainId), + String(Date.now()), + TRANSACTION_CACHE_TTL, + ); + } + } + + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + keysCount: keys.length, + }, + '[TxCache] MARKED for refresh - all chains', + ); + } + } catch (error) { + logger.error({ error }, '[TxCache] MARK_REFRESH'); + CacheMetrics.error(); + } + } + + /** + * Clear refresh flag for a predicate + */ + async clearRefreshFlag(predicateAddress: string, chainId: number): Promise { + try { + await RedisWriteClient.del([this.buildRefreshKey(predicateAddress, chainId)]); + } catch (error) { + logger.error({ error }, '[TxCache] CLEAR_REFRESH'); + } + } + + /** + * Legacy invalidate method - now marks for refresh instead of deleting + */ + async invalidate(predicateAddress: string, chainId?: number): Promise { + await this.markForRefresh(predicateAddress, chainId); + } + + /** + * Force delete cache (for admin/debug purposes) + */ + async forceDelete(predicateAddress: string, chainId?: number): Promise { + try { + if (chainId) { + const key = this.buildKey(predicateAddress, chainId); + await RedisWriteClient.del([key]); + await this.clearRefreshFlag(predicateAddress, chainId); + + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + chainId, + }, + '[TxCache] DELETED', + ); + } else { + const pattern = `${TRANSACTION_CACHE_PREFIX}:${predicateAddress}:*`; + const deletedCount = await RedisWriteClient.delByPattern(pattern); + + // Also delete refresh flags + const refreshPattern = `${TRANSACTION_CACHE_PREFIX}:refresh:${predicateAddress}:*`; + await RedisWriteClient.delByPattern(refreshPattern); + + logger.info( + { + predicateAddress: predicateAddress?.slice(0, 12), + deletedCount, + }, + '[TxCache] DELETED - all chains', + ); + } + } catch (error) { + logger.error({ error }, '[TxCache] DELETE'); + CacheMetrics.error(); + } + } + + /** + * Get the limit for incremental fetch + */ + getIncrementalFetchLimit(): number { + return INCREMENTAL_FETCH_LIMIT; + } + + /** + * Get cache statistics + */ + async stats(): Promise<{ + totalKeys: number; + refreshPending: number; + ttl: number; + incrementalLimit: number; + byChain: Record; + }> { + try { + const keys = await RedisReadClient.keys(`${TRANSACTION_CACHE_PREFIX}:*`); + const refreshKeys = keys.filter(k => k.includes(':refresh:')); + const cacheKeys = keys.filter(k => !k.includes(':refresh:')); + + const byChain: Record = {}; + for (const key of cacheKeys) { + const parts = key.split(':'); + const chainId = parseInt(parts[parts.length - 1], 10); + if (!isNaN(chainId)) { + byChain[chainId] = (byChain[chainId] || 0) + 1; + } + } + + return { + totalKeys: cacheKeys.length, + refreshPending: refreshKeys.length, + ttl: TRANSACTION_CACHE_TTL, + incrementalLimit: INCREMENTAL_FETCH_LIMIT, + byChain, + }; + } catch (error) { + logger.error({ error }, '[TxCache] STATS'); + return { + totalKeys: 0, + refreshPending: 0, + ttl: TRANSACTION_CACHE_TTL, + incrementalLimit: INCREMENTAL_FETCH_LIMIT, + byChain: {}, + }; + } + } + + /** + * Start the TransactionCache singleton + */ + static start(): TransactionCache { + if (!TransactionCache.instance) { + TransactionCache.instance = new TransactionCache(); + logger.info( + { + enabled: cacheConfig.enabled, + ttl: `${TRANSACTION_CACHE_TTL}s`, + incrementalLimit: INCREMENTAL_FETCH_LIMIT, + }, + '[TxCache] Started', + ); + } + return TransactionCache.instance; + } + + /** + * Stop the TransactionCache + */ + static stop(): void { + if (TransactionCache.instance) { + TransactionCache.instance = undefined; + logger.info('[TxCache] Stopped'); + } + } + + /** + * Get the singleton instance + */ + static getInstance(): TransactionCache { + if (!TransactionCache.instance) { + throw new Error('TransactionCache not started'); + } + return TransactionCache.instance; + } +} diff --git a/packages/api/src/server/tracing.ts b/packages/api/src/server/tracing.ts index 647ff3101..d7965c159 100644 --- a/packages/api/src/server/tracing.ts +++ b/packages/api/src/server/tracing.ts @@ -1,4 +1,5 @@ import { NodeSDK } from '@opentelemetry/sdk-node'; +import { logger } from '@src/config/logger'; import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; import { TypeormInstrumentation } from '@opentelemetry/instrumentation-typeorm'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; @@ -27,7 +28,7 @@ const sdk = new NodeSDK({ ], }); -if (process.env.API_ENVIRONMENT != 'development') { - console.log('[TELEMETRY] Starting'); +if (process.env.API_ENVIRONMENT === 'production') { + logger.info('[TELEMETRY] Starting'); sdk.start(); } diff --git a/packages/api/src/socket/client.ts b/packages/api/src/socket/client.ts index 459337ae3..e5bb8a0ee 100644 --- a/packages/api/src/socket/client.ts +++ b/packages/api/src/socket/client.ts @@ -1,6 +1,10 @@ import { io, Socket } from 'socket.io-client'; import { IMessage, SocketEvents, SocketUsernames } from './types'; +import { logger } from '@src/config/logger'; +const DEFAULT_DISCONNECT_TIMEOUT_MS = parseInt( + process.env.SOCKET_CLIENT_DISCONNECT_TIMEOUT || '30000', +); // Default => 30 seconds export class SocketClient { _socket: Socket = null; @@ -18,34 +22,77 @@ export class SocketClient { this._socket = io(URL, { autoConnect: true, auth }); } - private async _emitWhenConnected(event: string, data: any) { + /** + * Emit event when connected, then auto-disconnect after timeout to prevent memory leaks. + * + * @param event - Socket event name + * @param data - Event data payload + * @param timeoutDisconnect - Auto-disconnect timeout in milliseconds + */ + private async _emitWhenConnectedAndDisconnect( + event: string, + data: any, + timeoutDisconnect: number = DEFAULT_DISCONNECT_TIMEOUT_MS, + ): Promise { if (this._socket.connected) { + logger.info({ data }, '[EMIT WHEN CONNECTED AND DISCONNECT] Emitting event'); this._socket.emit(event, data); } else { await new Promise(resolve => { this._socket.once('connect', () => { + logger.info( + { data }, + '[EMIT WHEN CONNECTED AND DISCONNECT] Emitting event', + ); + this._socket.emit(event, data); resolve(); }); }); - this._socket.emit(event, data); } + + await new Promise(r => setTimeout(r, timeoutDisconnect)); + logger.info('[EMIT WHEN CONNECTED AND DISCONNECT] Disconnecting socket'); + this._socket.disconnect(); } - // Método para enviar uma mensagem para o servidor - async sendMessage(message: IMessage) { - await this._emitWhenConnected(SocketEvents.DEFAULT, message); + /** + * Send a message to the socket server. + * Automatically disconnects after default timeout. + * + * @param message - Message object conforming to IMessage + * @param timeoutDisconnect - Optional timeout override in milliseconds + */ + async sendMessage(message: IMessage, timeoutDisconnect?: number): Promise { + await this._emitWhenConnectedAndDisconnect( + SocketEvents.DEFAULT, + message, + timeoutDisconnect, + ); } - async emit(event: string, data: any) { - await this._emitWhenConnected(event, data); + /** + * Emit a custom event to the socket server. + * Automatically disconnects after default timeout. + * + * @param event - Socket event name + * @param data - Event data payload + * @param timeoutDisconnect - Optional timeout override in milliseconds + */ + async emit(event: string, data: any, timeoutDisconnect?: number): Promise { + await this._emitWhenConnectedAndDisconnect(event, data, timeoutDisconnect); } - // Método para desconectar do servidor Socket.IO - disconnect() { - this._socket.disconnect(); + /** + * Manual disconnect (rarely needed, auto-disconnect handles most cases). + * Keep for edge cases where immediate disconnect is required. + */ + disconnect(): void { + if (this._socket) { + this._socket.disconnect(); + } } - get socket() { + get socket(): Socket { return this._socket; } } diff --git a/packages/api/src/socket/events.ts b/packages/api/src/socket/events.ts index f6fc3a44b..b9eb7508c 100644 --- a/packages/api/src/socket/events.ts +++ b/packages/api/src/socket/events.ts @@ -1,6 +1,10 @@ -import { ITransactionResponse, ITransactionHistory } from "@src/modules/transaction/types"; -import { SocketClient } from "./client"; -import { SocketEvents } from "./types"; +import { + ITransactionResponse, + ITransactionHistory, +} from '@src/modules/transaction/types'; +import { SocketClient } from './client'; +import { SocketEvents } from './types'; + const { API_URL } = process.env; export type TransactionEvent = { @@ -9,9 +13,41 @@ export type TransactionEvent = { type: string; transaction: ITransactionResponse; history: ITransactionHistory[]; -} +}; + +export type BalanceOutdatedUserEvent = { + sessionId: string; + to: string; + type: SocketEvents; + workspaceId: string; + outdatedPredicateIds: string[]; +}; + +export type BalanceOutdatedPredicateEvent = { + sessionId: string; + to: string; + type: SocketEvents; + predicateId: string; + workspaceId: string; +}; export function emitTransaction(userId: string, data: TransactionEvent) { const socketClient = new SocketClient(userId, API_URL); - socketClient.socket.emit(SocketEvents.TRANSACTION, data); + socketClient.emit(SocketEvents.TRANSACTION, data); +} + +export function emitBalanceOutdatedUser( + userId: string, + data: BalanceOutdatedUserEvent, +) { + const socketClient = new SocketClient(userId, API_URL); + socketClient.emit(SocketEvents.BALANCE_OUTDATED_USER, data); +} + +export function emitBalanceOutdatedPredicate( + userId: string, + data: BalanceOutdatedPredicateEvent, +) { + const socketClient = new SocketClient(userId, API_URL); + socketClient.emit(SocketEvents.BALANCE_OUTDATED_PREDICATE, data); } diff --git a/packages/api/src/socket/types.ts b/packages/api/src/socket/types.ts index 24add87ce..36c927269 100644 --- a/packages/api/src/socket/types.ts +++ b/packages/api/src/socket/types.ts @@ -20,7 +20,6 @@ export enum SocketEvents { NOTIFICATION = 'notification', NEW_NOTIFICATION = '[NEW_NOTIFICATION]', - TRANSACTION_UPDATE = '[TRANSACTION]', VAULT_UPDATE = '[VAULT]', TRANSACTION = '[TRANSACTION]', @@ -29,6 +28,10 @@ export enum SocketEvents { TRANSACTION_CANCELED = '[CANCELED]', SWITCH_NETWORK = '[SWITCH_NETWORK]', + BALANCE_OUTDATED_USER = '[BALANCE_OUTDATED_USER]', + BALANCE_OUTDATED_PREDICATE = '[BALANCE_OUTDATED_PREDICATE]', + + AUTH_CONFIRMED = '[AUTH_CONFIRMED]', } export enum SocketUsernames { diff --git a/packages/api/src/tests/addressBook.tests.ts b/packages/api/src/tests/addressBook.tests.ts index fab44505a..1502a22a7 100644 --- a/packages/api/src/tests/addressBook.tests.ts +++ b/packages/api/src/tests/addressBook.tests.ts @@ -1,9 +1,8 @@ -import test from 'node:test'; import assert from 'node:assert/strict'; +import test from 'node:test'; import request from 'supertest'; import { TestEnvironment } from './utils/Setup'; -import { generateNode } from './mocks/Networks'; test('Address Book Endpoints', async t => { const { app, users, close } = await TestEnvironment.init(2, 0); @@ -17,7 +16,7 @@ test('Address Book Endpoints', async t => { await t.test('POST /address-book should create a new entry', async () => { const adb = { nickname: `Underground Seninha ${Date.now()}`, - address: users[1].payload.address, + address: users[0].payload.address, }; const res = await request(app) @@ -46,7 +45,7 @@ test('Address Book Endpoints', async t => { .send({ nickname: newNickname, id: createdId, - address: users[1].payload.address, + address: users[0].payload.address, }); assert.equal(res.status, 200); diff --git a/packages/api/src/tests/auth.tests.ts b/packages/api/src/tests/auth.tests.ts index bfe592774..c12b77351 100644 --- a/packages/api/src/tests/auth.tests.ts +++ b/packages/api/src/tests/auth.tests.ts @@ -2,7 +2,7 @@ import test from 'node:test'; import assert from 'node:assert'; import App from '@src/server/app'; import request from 'supertest'; -import { TypeUser } from '@src/models'; +import { TypeUser } from 'bakosafe'; import { newUser } from '@src/tests/mocks/User'; import { networks } from '@src/tests/mocks/Networks'; diff --git a/packages/api/src/tests/cliToken.tests.ts b/packages/api/src/tests/cliToken.tests.ts index a545e9291..588620980 100644 --- a/packages/api/src/tests/cliToken.tests.ts +++ b/packages/api/src/tests/cliToken.tests.ts @@ -7,25 +7,42 @@ import { cliTokenMock } from './mocks/Tokens'; import { generateNode } from './mocks/Networks'; test('Cli token', async t => { - const { provider, node } = await generateNode(); + const { node } = await generateNode(); const { close } = await TestEnvironment.init(2, 1, node); t.after(async () => { await close(); }); + const tokenCoder = new CLITokenCoder('aes-256-cbc'); - // await t.test('Encode', () => { - // const { data } = cliTokenMock; - // const encoded = tokenCoder.encode(data.apiToken, data.userId); - // assert.equal(encoded, cliTokenMock.encoded); - // }); - // await t.test('Decode', () => { - // const { data } = cliTokenMock; - // const decoded = tokenCoder.decode(cliTokenMock.encoded); - // assert.deepStrictEqual(decoded, data); - // }); - // await t.test('Decode with invalid token', () => { - // const decode = () => tokenCoder.decode('invalid_token'); - // assert.throws(() => decode(), /Invalid token/); - // }); + + await t.test('Encode and Decode should be reversible', () => { + const { data } = cliTokenMock; + const encoded = tokenCoder.encode(data.apiToken, data.userId); + + assert.ok(encoded); + assert.ok(typeof encoded === 'string'); + assert.ok(encoded.length > 0); + + const decoded = tokenCoder.decode(encoded); + assert.deepStrictEqual(decoded, data); + }); + + await t.test('Decode with invalid token should throw', () => { + assert.throws(() => tokenCoder.decode('invalid_token'), /Invalid token/); + }); + + await t.test('Decode with empty token should throw', () => { + assert.throws(() => tokenCoder.decode(''), /Invalid token/); + }); + + await t.test( + 'Encode should produce different output for different inputs', + () => { + const encoded1 = tokenCoder.encode('token1', 'user1'); + const encoded2 = tokenCoder.encode('token2', 'user2'); + + assert.notStrictEqual(encoded1, encoded2); + }, + ); }); diff --git a/packages/api/src/tests/connections.tests.ts b/packages/api/src/tests/connections.tests.ts new file mode 100644 index 000000000..8d84a11e4 --- /dev/null +++ b/packages/api/src/tests/connections.tests.ts @@ -0,0 +1,153 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import request from 'supertest'; + +import { TestEnvironment } from './utils/Setup'; +import { generateNode } from './mocks/Networks'; +import { saveMockPredicate } from './mocks/Predicate'; + +test('Connections/dApps Endpoints', async t => { + const { node } = await generateNode(); + + const { app, users, predicates, close } = await TestEnvironment.init(2, 1, node); + + const vault = predicates[0]; + const { predicate } = await saveMockPredicate(vault, users[0], app); + + t.after(async () => { + await close(); + }); + + const testSessionId = `test-session-${Date.now()}`; + + await t.test( + 'GET /connections/:sessionId/state should return false for non-existent session', + async () => { + const res = await request(app) + .get(`/connections/${testSessionId}/state`) + .set('origin', 'http://localhost:5174'); + + assert.equal(res.status, 200); + assert.equal(res.body, false); + }, + ); + + await t.test( + 'GET /connections/:sessionId/currentAccount should return null for non-existent session', + async () => { + const res = await request(app) + .get(`/connections/${testSessionId}/currentAccount`) + .set('origin', 'http://localhost:5174'); + + assert.equal(res.status, 200); + assert.equal(res.body, null); + }, + ); + + await t.test( + 'GET /connections/:sessionId/currentNetwork should return null for non-existent session', + async () => { + const res = await request(app) + .get(`/connections/${testSessionId}/currentNetwork`) + .set('origin', 'http://localhost:5174'); + + assert.equal(res.status, 200); + assert.equal(res.body, null); + }, + ); + + await t.test( + 'POST /connections should create a new dApp connection', + async () => { + const connectionPayload = { + vaultId: predicate.id, + sessionId: testSessionId, + name: 'Test dApp', + origin: 'http://localhost:5174', + userAddress: users[0].payload.address, + request_id: `req-${Date.now()}`, + }; + + const res = await request(app) + .post('/connections') + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address) + .send(connectionPayload); + + assert.equal(res.status, 201); + assert.equal(res.body, true); + }, + ); + + await t.test( + 'GET /connections/:sessionId/state should return true after connection', + async () => { + const res = await request(app) + .get(`/connections/${testSessionId}/state`) + .set('origin', 'http://localhost:5174'); + + assert.equal(res.status, 200); + assert.equal(res.body, true); + }, + ); + + await t.test( + 'GET /connections/:sessionId/currentAccount should return vault address after connection', + async () => { + const res = await request(app) + .get(`/connections/${testSessionId}/currentAccount`) + .set('origin', 'http://localhost:5174'); + + assert.equal(res.status, 200); + assert.equal(res.body, predicate.predicateAddress); + }, + ); + + await t.test( + 'GET /connections/:sessionId/accounts should return vault addresses', + async () => { + const res = await request(app) + .get(`/connections/${testSessionId}/accounts`) + .set('origin', 'http://localhost:5174'); + + assert.equal(res.status, 200); + assert.ok(Array.isArray(res.body)); + assert.ok(res.body.includes(predicate.predicateAddress)); + }, + ); + + await t.test( + 'GET /connections/:sessionId should return current vault address', + async () => { + const res = await request(app) + .get(`/connections/${testSessionId}`) + .set('origin', 'http://localhost:5174'); + + assert.equal(res.status, 200); + assert.equal(res.body, predicate.predicateAddress); + }, + ); + + await t.test( + 'DELETE /connections/:sessionId should disconnect the dApp', + async () => { + const res = await request(app) + .delete(`/connections/${testSessionId}`) + .set('origin', 'http://localhost:5174'); + + assert.equal(res.status, 204); + }, + ); + + await t.test( + 'GET /connections/:sessionId/state should return false after disconnect', + async () => { + const res = await request(app) + .get(`/connections/${testSessionId}/state`) + .set('origin', 'http://localhost:5174'); + + assert.equal(res.status, 200); + assert.equal(res.body, false); + }, + ); +}); diff --git a/packages/api/src/tests/meld.tests.ts b/packages/api/src/tests/meld.tests.ts new file mode 100644 index 000000000..6a48d19c1 --- /dev/null +++ b/packages/api/src/tests/meld.tests.ts @@ -0,0 +1,207 @@ +import { MeldApiFactory } from '@src/modules/meld/utils'; +import { logger } from '@src/config/logger'; +import assert from 'node:assert/strict'; +import test from 'node:test'; +import request from 'supertest'; +import { + getValidMeldSignature, + meldTransactionListMock, + mockQuotesResponse, + widgetSessionMock, +} from './mocks/Meld'; + +import { RampTransactionProvider } from '@src/models/RampTransactions'; +import { UnauthorizedErrorTitles } from '@src/utils/error'; +import axios from 'axios'; +import { generateNode } from './mocks/Networks'; +import { saveMockPredicate } from './mocks/Predicate'; +import { TestEnvironment } from './utils/Setup'; + +test('On Ramp endpoints', async t => { + const { node } = await generateNode(); + const { app, users, close, predicates } = await TestEnvironment.init(1, 1, node); + const user = users?.[0]; + const vault = predicates?.[0]; + + let widgetResponse: request.Response; + const sandboxSecret = process.env.MELD_SANDBOX_WEBHOOK_SECRET; + const productionSecret = process.env.MELD_PRODUCTION_WEBHOOK_SECRET; + + t.before(async () => { + await saveMockPredicate(vault, user, app); + process.env.MELD_SANDBOX_WEBHOOK_SECRET = 'test_secret'; + process.env.MELD_PRODUCTION_WEBHOOK_SECRET = 'test_secret'; + }); + + t.after(async () => { + await close(); + // Restore all mocks after tests complete + test.mock.restoreAll(); + process.env.MELD_SANDBOX_WEBHOOK = sandboxSecret; + process.env.MELD_PRODUCTION_WEBHOOK = productionSecret; + }); + + await t.test('should get meld quotes', async () => { + const mock = t.mock.method(MeldApiFactory, 'getMeldApiByNetwork', () => { + logger.info('🚀 getMeldQuotes MOCK CALLED'); + return { + getMeldQuotes: async () => mockQuotesResponse(), + }; + }); + const quoteRes = await request(app) + .post('/ramp-transactions/meld/quotes') + .set('Authorization', user.token) + .set('Signeraddress', user.payload.address) + .send({ + countryCode: 'BR', + destinationCurrencyCode: 'ETH', + sourceAmount: 100, + sourceCurrencyCode: 'BRL', + paymentMethodType: 'PIX', + }); + + assert.equal(mock.mock.calls.length, 1); + assert.equal(quoteRes.status, 200); + assert.ok(Array.isArray(quoteRes.body.quotes)); + assert.equal(quoteRes.body.quotes[0].destinationCurrencyCode, 'ETH'); + }); + + await t.test('should create meld widget session', async () => { + const mock = t.mock.method(MeldApiFactory, 'getMeldApiByNetwork', () => { + logger.info('🎯 createMeldWidgetSession MOCK CALLED'); + return { createMeldWidgetSession: async () => widgetSessionMock }; + }); + + const widgetRes = await request(app) + .post('/ramp-transactions/meld/widget') + .set('Authorization', user.token) + .set('Signeraddress', user.payload.address) + .send({ + type: 'BUY', + countryCode: 'BR', + destinationCurrencyCode: 'ETH', + serviceProvider: 'TEST', + sourceAmount: '100,00', + destinationAmount: '0.0742', + sourceCurrencyCode: 'BRL', + paymentMethodType: 'PIX', + walletAddress: vault.address.b256Address, + }); + + assert.equal(mock.mock.calls.length, 1); + assert.equal(widgetRes.status, 201); + assert.equal(widgetRes.body.provider, RampTransactionProvider.MELD); + assert.equal( + widgetRes.body.providerData?.widgetSessionData?.widgetUrl, + widgetSessionMock.widgetUrl, + ); + + widgetResponse = widgetRes; + }); + + await t.test('should get created ramp transaction', async () => { + const rampTxId = widgetResponse.body.id; + const fetchRampRes = await request(app) + .get(`/ramp-transactions/${rampTxId}`) + .set('Authorization', user.token) + .set('Signeraddress', user.payload.address); + + assert.equal(fetchRampRes.status, 200); + assert.equal(fetchRampRes.body.id, rampTxId); + assert.equal(fetchRampRes.body.widgetUrl, widgetSessionMock.widgetUrl); + assert.equal(fetchRampRes.body.status, 'IDLE'); + }); + + await t.test("should process meld's webhook and validate signature", async () => { + const mock = t.mock.method(axios, 'create', () => { + logger.info('🎯 getMeldTransactions MOCK CALLED'); + return { get: async () => ({ data: meldTransactionListMock }) }; + }); + + // Use a fixed timestamp for both payload and signature + const timestamp = new Date().toISOString(); + + const webhookPayload = { + eventType: 'PAYMENT_TRANSACTION_STATUS_CHANGED', + eventId: 'evt-1', + timestamp: timestamp, + accountId: 'acc-1', + version: '1', + payload: { + accountId: 'acc-1', + paymentTransactionId: 'ptx-1', + externalSessionId: + widgetResponse.body.providerData.widgetSessionData.externalSessionId, + paymentTransactionStatus: 'SETTLED', + }, + }; + + const meldSignature = getValidMeldSignature( + process.env.MELD_SANDBOX_WEBHOOK_SECRET!, + timestamp, + webhookPayload, + ); + + const webhookRes = await request(app) + .post('/webhooks/meld/crypto') + .set('host', '127.0.0.1') + .set('meld-signature', meldSignature) + .set('meld-signature-timestamp', timestamp) + .send(webhookPayload); + + assert.equal(mock.mock.calls.length, 1); + assert.equal(webhookRes.status, 200); + assert.equal(webhookRes.body.message, 'Webhook processed successfully'); + }); + + await t.test( + 'should have updated ramp transaction status to SETTLED', + async () => { + const rampTxId = widgetResponse.body.id; + const fetchRampRes = await request(app) + .get(`/ramp-transactions/${rampTxId}`) + .set('Authorization', user.token) + .set('Signeraddress', user.payload.address); + + assert.equal(fetchRampRes.status, 200); + assert.equal(fetchRampRes.body.id, rampTxId); + assert.equal(fetchRampRes.body.widgetUrl, widgetSessionMock.widgetUrl); + assert.equal(fetchRampRes.body.status, 'SETTLED'); + }, + ); + + await t.test('should reject webhook with invalid meld signature', async () => { + // Use a fixed timestamp for payload + const timestamp = new Date().toISOString(); + + const webhookPayload = { + eventType: 'PAYMENT_TRANSACTION_STATUS_CHANGED', + eventId: 'evt-1', + timestamp: timestamp, + accountId: 'acc-1', + version: '1', + payload: { + accountId: 'acc-1', + paymentTransactionId: 'ptx-1', + externalSessionId: 'test-session-id', + paymentTransactionStatus: 'SETTLED', + }, + }; + + const invalidSignature = 'invalid-signature-123'; + + const webhookRes = await request(app) + .post('/webhooks/meld/crypto') + .set('host', '127.0.0.1') + .set('meld-signature', invalidSignature) + .set('meld-signature-timestamp', timestamp) + .send(webhookPayload); + + assert.equal(webhookRes.status, 401); + assert.equal(webhookRes.body.origin, 'app'); + assert.equal( + webhookRes.body.errors.title, + UnauthorizedErrorTitles.INVALID_SIGNATURE, + ); + }); +}); diff --git a/packages/api/src/tests/mocks/Meld.ts b/packages/api/src/tests/mocks/Meld.ts new file mode 100644 index 000000000..ec08548c3 --- /dev/null +++ b/packages/api/src/tests/mocks/Meld.ts @@ -0,0 +1,85 @@ +import crypto from 'crypto'; + +export const mockQuotesResponse = (overrides: Partial = {}) => ({ + quotes: [ + { + countryCode: 'BR', + customerScore: 1, + destinationAmount: 0.02, + destinationAmountWithoutFees: 0.021, + destinationCurrencyCode: 'ETH', + exchangeRate: 10000, + fiatAmountWithoutFees: 100, + institutionName: 'TEST BANK', + lowKyc: true, + networkFee: 0.001, + partnerFee: 0.001, + paymentMethodType: 'PIX', + serviceProvider: 'TEST', + sourceAmount: 100, + sourceAmountWithoutFees: 99, + sourceCurrencyCode: 'BRL', + totalFee: 1, + transactionFee: 0.5, + transactionType: 'BUY', + ...overrides, + }, + ], +}); + +export const widgetSessionMock = { + id: 'sess-1', + token: 'token-abc', + widgetUrl: 'https://widget.mock', + externalSessionId: 'ext-session-1', + externalCustomerId: 'ext-cust-1', + customerId: 'cust-1', +}; + +export const meldTransactionListMock = { + transactions: [ + { + id: 'tr-1', + destinationAmount: 0.02, + destinationCurrencyCode: 'ETH', + serviceProvider: 'TEST', + status: 'SETTLED', + countryCode: 'BR', + cryptoDetails: { + blockchainTransactionId: '0xMOCKCHAINID', + chainId: '0', + destinationWalletAddress: '0xabc', + institution: 'TEST', + networkFee: 0.001, + networkFeeInUsd: 0.001, + partnerFee: 0.001, + partnerFeeInUsd: 0.001, + sessionWalletAddress: '0xabc', + sourceWalletAddress: '0xabc', + totalFee: 0.002, + totalFeeInUsd: 0.002, + transactionFee: 0.001, + transactionFeeInUsd: 0.001, + }, + }, + ], +}; + +export const getValidMeldSignature = ( + secret: string, + timestamp: string, + body: unknown, +) => { + // Generate valid signature that matches what the middleware expects + const protocol = 'https'; + const host = '127.0.0.1'; + const originalUrl = '/webhooks/meld/crypto'; + const url = `${protocol}://${host}${originalUrl}`; + const bodyString = JSON.stringify(body); + const signedPayload = `${timestamp}.${url}.${bodyString}`; + return crypto + .createHmac('sha256', secret) + .update(signedPayload) + .digest('base64url') + .replace(/=/g, ''); +}; diff --git a/packages/api/src/tests/mocks/Networks.ts b/packages/api/src/tests/mocks/Networks.ts index 913df120f..910774ec8 100644 --- a/packages/api/src/tests/mocks/Networks.ts +++ b/packages/api/src/tests/mocks/Networks.ts @@ -9,7 +9,7 @@ export const networks = { }; export const generateNode = async () => { - let node = await launchTestNode({ + const node = await launchTestNode({ walletsConfig: { assets: assets(), coinsPerAsset: 1, @@ -20,7 +20,8 @@ export const generateNode = async () => { }, }); - await deployPredicate(node.wallets[0]); + const wallet = node.wallets[0]; + await deployPredicate(wallet); return { node, diff --git a/packages/api/src/tests/mocks/User.ts b/packages/api/src/tests/mocks/User.ts index 4f9283c3f..c632ea408 100644 --- a/packages/api/src/tests/mocks/User.ts +++ b/packages/api/src/tests/mocks/User.ts @@ -1,5 +1,5 @@ import { networks } from '@src/tests/mocks/Networks'; -import { TypeUser } from '@src/models'; +import { TypeUser } from 'bakosafe'; import { WalletUnlocked } from 'fuels'; export const newUser = () => { diff --git a/packages/api/src/tests/mocks/predicate-release/bako-predicate-loader-bin-root b/packages/api/src/tests/mocks/predicate-release/bako-predicate-loader-bin-root index e9da8069c..73f0186b7 100644 --- a/packages/api/src/tests/mocks/predicate-release/bako-predicate-loader-bin-root +++ b/packages/api/src/tests/mocks/predicate-release/bako-predicate-loader-bin-root @@ -1 +1 @@ -0x6ca3bcd759b944b128e9007e2fa75bf700f28c39ce7b34fc241e2c57bf02bdff \ No newline at end of file +0x967aaa71b3db34acd8104ed1d7ff3900e67cff3d153a0ffa86d85957f579aa6a \ No newline at end of file diff --git a/packages/api/src/tests/predicate.tests.ts b/packages/api/src/tests/predicate.tests.ts index c29fd1c62..36c746c3e 100644 --- a/packages/api/src/tests/predicate.tests.ts +++ b/packages/api/src/tests/predicate.tests.ts @@ -1,11 +1,10 @@ -import test from 'node:test'; import assert from 'node:assert/strict'; +import test from 'node:test'; import request from 'supertest'; -import { TestEnvironment } from './utils/Setup'; -import { ZeroBytes32 } from 'fuels'; -import { saveMockPredicate } from './mocks/Predicate'; import { generateNode } from './mocks/Networks'; +import { saveMockPredicate } from './mocks/Predicate'; +import { TestEnvironment } from './utils/Setup'; test('Predicate Endpoints', async t => { const { node } = await generateNode(); @@ -44,10 +43,10 @@ test('Predicate Endpoints', async t => { assert.equal(res.body.description, payload.description); assert.equal(res.body.predicateAddress, predicateAddress); assert.equal(res.body.owner.address, users[0].payload.address); - assert.equal( - res.body.members.length, - vault.configurable.SIGNERS.filter(i => i != ZeroBytes32).length, - ); + // assert.equal( + // res.body.members.length, + // vault.configurable.SIGNERS.filter(i => i != ZeroBytes32).length, + // ); }, ); @@ -79,10 +78,10 @@ test('Predicate Endpoints', async t => { assert.deepEqual(res.body.configurable, configurable); assert.ok('address' in res.body.owner); assert.strictEqual(res.body.owner.address, users[0].payload.address); - assert.equal( - res.body.members.length, - vault.configurable.SIGNERS.filter(i => i != ZeroBytes32).length, - ); + // assert.equal( + // res.body.members.length, + // vault.configurable?.SIGNERS.filter(i => i != ZeroBytes32).length, + // ); assert.ok('avatar' in res.body.members[0]); assert.ok('address' in res.body.members[0]); assert.ok('type' in res.body.members[0]); @@ -219,6 +218,22 @@ test('Predicate Endpoints', async t => { }, ); + // IMPORTANT: necessary to ensure backward compatibility of the connector with EVM Wallets + await t.test( + 'GET /predicate/by-address/:address should return 404 when predicate address is not found', + async () => { + // Generate an invalid predicate address that doesn't exist + const invalidAddress = '0x' + 'f'.repeat(64); + + const res = await request(app) + .get(`/predicate/by-address/${invalidAddress}`) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address); + + assert.equal(res.status, 404); + }, + ); + await t.test( 'GET /predicate/reserved-coins/:id should find predicate balance', async () => { @@ -241,6 +256,24 @@ test('Predicate Endpoints', async t => { }, ); + await t.test( + "GET /predicate/:predicateId/allocation should get predicate's allocation", + async () => { + const vault = predicates[0]; + + const { predicate } = await saveMockPredicate(vault, users[0], app); + + const res = await request(app) + .get(`/predicate/${predicate.id}/allocation`) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address); + assert.equal(res.status, 200); + assert.ok(Array.isArray(res.body.data)); + assert.ok(Array.isArray(res.body.predicates)); + assert.strictEqual(typeof res.body.totalAmountInUSD, 'number'); + }, + ); + await t.test( 'GET /check/by-address/:address should check if predicate exists by addr', async () => { @@ -274,4 +307,49 @@ test('Predicate Endpoints', async t => { assert.ok(res.body.includes(predicate.predicateAddress)); }, ); + + await t.test('PUT /predicate/:predicateId should update predicate', async () => { + const vault = predicates[0]; + + const { predicate } = await saveMockPredicate(vault, users[0], app); + + const payload = { + name: `Updated Name ${Date.now()}`, + description: 'Updated description', + }; + + const res = await request(app) + .put(`/predicate/${predicate.id}`) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address) + .send(payload); + + assert.equal(res.status, 200); + assert.strictEqual(res.body.id, predicate.id); + assert.strictEqual(res.body.name, payload.name); + assert.strictEqual(res.body.description, payload.description); + }); + + await t.test( + 'PUT /predicate/:predicateId should not update predicate when dont pass name', + async () => { + const vault = predicates[0]; + + const { predicate } = await saveMockPredicate(vault, users[0], app); + + const payload = {}; + + const res = await request(app) + .put(`/predicate/${predicate.id}`) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address) + .send(payload); + + assert.equal(res.status, 400); + assert.ok('errors' in res.body); + assert.ok('detail' in res.body.errors[0]); + assert.ok('title' in res.body.errors[0]); + assert.strictEqual(res.body.errors[0].title, '"name" is required'); + }, + ); }); diff --git a/packages/api/src/tests/transaction.tests.ts b/packages/api/src/tests/transaction.tests.ts index 850274705..4d3375a05 100644 --- a/packages/api/src/tests/transaction.tests.ts +++ b/packages/api/src/tests/transaction.tests.ts @@ -4,7 +4,6 @@ import request from 'supertest'; import { TestEnvironment } from './utils/Setup'; import { saveMockPredicate } from './mocks/Predicate'; -import { ZeroBytes32 } from 'fuels'; import { saveMockTransaction, transactionMock } from '@src/tests/mocks/Transaction'; import { TransactionStatus, TransactionType, WitnessStatus } from 'bakosafe'; import { Transaction } from '@src/models'; @@ -15,7 +14,7 @@ test('Transaction Endpoints', async t => { const { app, users, predicates, wallets, close } = await TestEnvironment.init( 5, - 5, + 6, node, ); @@ -55,10 +54,10 @@ test('Transaction Endpoints', async t => { assert.strictEqual(resTx.predicate.predicateAddress, vault.address.toB256()); assert.ok(resTx.assets); assert.ok(resTx.resume.witnesses); - assert.equal( - resTx.resume.witnesses.length, - vault.configurable.SIGNERS.filter(i => i != ZeroBytes32).length, - ); + // assert.equal( + // resTx.resume.witnesses.length, + // vault.configurable.SIGNERS.filter(i => i != ZeroBytes32).length, + // ); }); await t.test('GET /transaction should list transactions', async () => { @@ -95,8 +94,6 @@ test('Transaction Endpoints', async t => { .set('Authorization', users[0].token) .set('Signeraddress', users[0].payload.address); - // console.log('[TESTE_PAGINACAO]', res.body); - assert.equal(res.status, 200); assert.ok('total' in res.body); assert.equal(res.body.currentPage, page); @@ -196,7 +193,9 @@ test('Transaction Endpoints', async t => { const member = predicate.members.find( member => member.id === element.owner.id, ); - assert.deepStrictEqual(element.owner, member); + for (const key of Object.keys(element.owner)) { + assert.deepStrictEqual(member[key], element.owner[key]); + } }); }, ); @@ -204,29 +203,33 @@ test('Transaction Endpoints', async t => { await t.test( 'GET /transaction/pending should get transactions pending', async () => { - const vault = predicates[2]; - await saveMockPredicate(vault, users[2], app); + const user = users[0]; - const { payload_transfer } = await transactionMock(vault); + // Capture the count before creating the transaction + const resBefore = await request(app) + .get(`/transaction/pending`) + .set('Authorization', user.token) + .set('Signeraddress', user.payload.address); - payload_transfer.status = TransactionStatus.PENDING_SENDER; + assert.equal(resBefore.status, 200); + const previousCount = resBefore.body.ofUser; - await request(app) - .post('/transaction') - .set('Authorization', users[0].token) - .set('Signeraddress', users[0].payload.address) - .send(payload_transfer); + const vault = predicates[predicates.length - 1]; + const { tx, status } = await saveMockTransaction({ vault, user }, app); + + assert.equal(status, 201); + assert.equal(tx.status, TransactionStatus.AWAIT_REQUIREMENTS); const res = await request(app) .get(`/transaction/pending`) - .set('Authorization', users[0].token) - .set('Signeraddress', users[0].payload.address); + .set('Authorization', user.token) + .set('Signeraddress', user.payload.address); assert.equal(res.status, 200); assert.ok('ofUser' in res.body); assert.ok('transactionsBlocked' in res.body); assert.ok('pendingSignature' in res.body); - assert.strictEqual(res.body.ofUser, 1); + assert.strictEqual(res.body.ofUser, previousCount + 1); assert.strictEqual(res.body.transactionsBlocked, true); assert.strictEqual(res.body.pendingSignature, true); }, @@ -419,6 +422,27 @@ test('Transaction Endpoints', async t => { }, ); + await t.test( + 'DELETE /transaction/by-hash/:hash should delete latest transaction by hash', + async () => { + const vault = predicates[3]; + + const { tx: transaction, status } = await saveMockTransaction( + { vault: vault, user: users[3] }, + app, + ); + + assert.equal(status, 201); + + const res = await request(app) + .delete(`/transaction/by-hash/${transaction.hash}`) + .set('Authorization', users[0].token) + .set('Signeraddress', users[0].payload.address); + + assert.equal(res.status, 200); + }, + ); + await t.test( 'Should correctly handle new transaction with same hash as canceled transaction', async () => { @@ -428,7 +452,7 @@ test('Transaction Endpoints', async t => { // Step 1: Create a transaction const { payload_transfer: payload1 } = await transactionMock(vault); - const { body: createdTx1, status: statusCreate1 } = await request(app) + const { body: createdTx1 } = await request(app) .post('/transaction') .set('Authorization', user.token) .set('Signeraddress', user.payload.address) @@ -447,7 +471,7 @@ test('Transaction Endpoints', async t => { assert.equal(canceledTx.status, TransactionStatus.CANCELED); // Step 3: Create a new transaction with the same hash (identical) - const { body: createdTx2, status: statusCreate2 } = await request(app) + const { body: createdTx2 } = await request(app) .post('/transaction') .set('Authorization', user.token) .set('Signeraddress', user.payload.address) diff --git a/packages/api/src/tests/user.tests.ts b/packages/api/src/tests/user.tests.ts index 5d3efca47..f0387588b 100644 --- a/packages/api/src/tests/user.tests.ts +++ b/packages/api/src/tests/user.tests.ts @@ -1,17 +1,41 @@ -import test from 'node:test'; import assert from 'node:assert/strict'; +import test from 'node:test'; import request from 'supertest'; -import { TestEnvironment } from './utils/Setup'; import { generateNode } from './mocks/Networks'; +import { saveMockPredicate } from './mocks/Predicate'; +import { TestEnvironment } from './utils/Setup'; +import { TransactionStatus, TransactionType } from 'bakosafe'; test('User Endpoints', async t => { - const { app, users, close } = await TestEnvironment.init(2, 0); + const { node } = await generateNode(); + const { app, users, close, predicates, network } = await TestEnvironment.init( + 2, + 1, + node, + ); t.after(async () => { await close(); }); + await t.test( + 'POST /user/select-network should return network object with url and chainId', + async () => { + const res = await request(app) + .post('/user/select-network') + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address) + .send({ network: network.url }); + + assert.equal(res.status, 200); + assert.ok('url' in res.body, 'Response should contain url'); + assert.ok('chainId' in res.body, 'Response should contain chainId'); + assert.equal(typeof res.body.url, 'string'); + assert.equal(typeof res.body.chainId, 'number'); + }, + ); + await t.test('PUT /user/:id should update the entry nickname', async () => { const newName = `${new Date().getTime()} - Update user test`; @@ -41,10 +65,57 @@ test('User Endpoints', async t => { ); await t.test( - 'GET /user/latest/transactions should list home user transactions', + 'GET /user/transactions should list home user transactions', + async () => { + const res = await request(app) + .get('/user/transactions') + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address); + + assert.equal(res.status, 200); + assert.ok('data' in res.body); + assert.ok(Array.isArray(res.body.data)); + }, + ); + + await t.test( + 'GET /user/transactions should accept status query param', + async () => { + const res = await request(app) + .get('/user/transactions') + .query({ status: ['await_requirements', 'pending_sender'] }) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address); + + assert.equal(res.status, 200); + assert.ok('data' in res.body); + assert.ok(Array.isArray(res.body.data)); + }, + ); + + await t.test( + 'GET /user/transactions should accept type query param', async () => { const res = await request(app) - .get('/user/latest/transactions') + .get('/user/transactions') + .query({ type: 'DEPOSIT' }) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address); + + assert.equal(res.status, 200); + assert.ok('data' in res.body); + assert.ok(Array.isArray(res.body.data)); + }, + ); + + await t.test( + 'GET /user/transactions should accept both status and type query params', + async () => { + const status = TransactionStatus.SUCCESS; + const type = TransactionType.TRANSACTION_SCRIPT; + + const res = await request(app) + .get(`/user/transactions?status[]=${status}&type=${type}`) .set('Authorization', users[0].token) .set('signeraddress', users[0].payload.address); @@ -63,4 +134,128 @@ test('User Endpoints', async t => { assert.equal(res.status, 200); assert.ok(Array.isArray(res.body)); }); + + await t.test( + "GET /user/allocation should get user's asset allocation", + async () => { + const vault = predicates[0]; + await saveMockPredicate(vault, users[0], app); + + const res = await request(app) + .get('/user/allocation') + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address); + + assert.equal(res.status, 200); + assert.ok(Array.isArray(res.body.data)); + assert.ok('totalAmountInUSD' in res.body); + assert.ok('predicates' in res.body); + assert.ok(Array.isArray(res.body.predicates)); + }, + ); + + await t.test( + 'GET /user/allocation should return predicates with correct structure', + async () => { + const res = await request(app) + .get('/user/allocation') + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address); + + assert.equal(res.status, 200); + + // Validate response structure + assert.ok('data' in res.body, 'Response should have data array'); + assert.ok( + 'totalAmountInUSD' in res.body, + 'Response should have totalAmountInUSD', + ); + assert.ok('predicates' in res.body, 'Response should have predicates array'); + + assert.ok(Array.isArray(res.body.data), 'data should be an array'); + assert.ok( + Array.isArray(res.body.predicates), + 'predicates should be an array', + ); + assert.equal( + typeof res.body.totalAmountInUSD, + 'number', + 'totalAmountInUSD should be a number', + ); + + // Validate predicate structure if any exist + if (res.body.predicates.length > 0) { + const predicate = res.body.predicates[0]; + assert.ok('id' in predicate, 'Predicate should have id'); + assert.ok('name' in predicate, 'Predicate should have name'); + assert.ok('address' in predicate, 'Predicate should have address'); + assert.ok('members' in predicate, 'Predicate should have members count'); + assert.ok( + 'minSigners' in predicate, + 'Predicate should have minSigners count', + ); + assert.ok('amountInUSD' in predicate, 'Predicate should have amountInUSD'); + + assert.equal(typeof predicate.id, 'string', 'id should be string'); + assert.equal(typeof predicate.name, 'string', 'name should be string'); + assert.equal( + typeof predicate.address, + 'string', + 'address should be string', + ); + assert.equal( + typeof predicate.members, + 'number', + 'members should be number', + ); + assert.equal( + typeof predicate.minSigners, + 'number', + 'minSigners should be number', + ); + assert.equal( + typeof predicate.amountInUSD, + 'number', + 'amountInUSD should be number', + ); + } + + // Validate asset allocation structure if any exist + if (res.body.data.length > 0) { + const allocation = res.body.data[0]; + assert.ok('assetId' in allocation, 'Allocation should have assetId'); + assert.ok( + 'amountInUSD' in allocation, + 'Allocation should have amountInUSD', + ); + assert.ok('percentage' in allocation, 'Allocation should have percentage'); + + assert.equal( + typeof allocation.amountInUSD, + 'number', + 'amountInUSD should be number', + ); + assert.equal( + typeof allocation.percentage, + 'number', + 'percentage should be number', + ); + } + }, + ); + + await t.test('GET /user/allocation should accept limit query param', async () => { + const res = await request(app) + .get('/user/allocation') + .query({ limit: 3 }) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address); + + assert.equal(res.status, 200); + assert.ok(Array.isArray(res.body.predicates)); + assert.ok( + res.body.predicates.length <= 3, + 'Should return at most 3 predicates', + ); + }); }); diff --git a/packages/api/src/tests/utils/Auth.ts b/packages/api/src/tests/utils/Auth.ts index 29a6844e8..98a336d6e 100644 --- a/packages/api/src/tests/utils/Auth.ts +++ b/packages/api/src/tests/utils/Auth.ts @@ -1,7 +1,7 @@ import request from 'supertest'; import { Wallet } from 'fuels'; import App from '@src/server/app'; -import { TypeUser } from '@src/models'; +import { TypeUser } from 'bakosafe'; import { Provider as FuelProvider } from 'fuels'; export class TestSession { diff --git a/packages/api/src/tests/utils/Setup.ts b/packages/api/src/tests/utils/Setup.ts index ae6054c14..f3b4fc57b 100644 --- a/packages/api/src/tests/utils/Setup.ts +++ b/packages/api/src/tests/utils/Setup.ts @@ -1,10 +1,10 @@ import { Application } from 'express'; +import { logger } from '@src/config/logger'; import request from 'supertest'; import { newUser } from '@src/tests/mocks/User'; -import { WalletUnlocked, Wallet, Provider } from 'fuels'; -import { TypeUser } from '@src/models'; +import { WalletUnlocked, Provider } from 'fuels'; import App from '@src/server/app'; -import { Vault } from 'bakosafe'; +import { Vault, TypeUser } from 'bakosafe'; import { getPredicateVersion } from '../mocks/Predicate'; import { DeployContractConfig, LaunchTestNodeReturn } from 'fuels/test-utils'; import { networks } from '../mocks/Networks'; @@ -100,7 +100,7 @@ export class TestEnvironment { version, ); - await (await wallets[0].transfer(vault.address, 1000)).waitForResult(); + await (await wallets[0].transfer(vault.address, 100_000)).waitForResult(); predicates.push(vault); } @@ -108,9 +108,9 @@ export class TestEnvironment { try { await App.stop(); node?.cleanup(); - console.log('Cleanup finalizado com sucesso.'); + logger.info('Cleanup completed successfully.'); } catch (error) { - console.error('Erro durante cleanup:', error); + logger.error({ error: error }, 'Error during cleanup'); } }; diff --git a/packages/api/src/tests/workspace.tests.ts b/packages/api/src/tests/workspace.tests.ts new file mode 100644 index 000000000..535ff6221 --- /dev/null +++ b/packages/api/src/tests/workspace.tests.ts @@ -0,0 +1,168 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import request from 'supertest'; + +import { TestEnvironment } from './utils/Setup'; +import { generateNode } from './mocks/Networks'; +import { PermissionRoles } from '@src/models/Workspace'; + +test('Workspace Endpoints', async t => { + const { node } = await generateNode(); + + const { app, users, close } = await TestEnvironment.init(3, 0, node); + + t.after(async () => { + await close(); + }); + + let createdWorkspaceId: string; + + await t.test('POST /workspace should create a new workspace', async () => { + const workspacePayload = { + name: `Test Workspace ${Date.now()}`, + description: 'A test workspace for e2e tests', + members: [], + }; + + const res = await request(app) + .post('/workspace') + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address) + .send(workspacePayload); + + assert.equal(res.status, 201); + assert.ok(res.body.id); + assert.equal(res.body.name, workspacePayload.name); + assert.equal(res.body.description, workspacePayload.description); + assert.ok(res.body.owner); + assert.equal(res.body.owner.id, users[0].id); + + createdWorkspaceId = res.body.id; + }); + + await t.test('GET /workspace/by-user should list user workspaces', async () => { + const res = await request(app) + .get('/workspace/by-user') + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address); + + assert.equal(res.status, 200); + assert.ok(Array.isArray(res.body)); + assert.ok(res.body.length >= 1); + + const workspace = res.body.find(w => w.id === createdWorkspaceId); + assert.ok(workspace); + }); + + await t.test('GET /workspace/:id should find workspace by id', async () => { + const res = await request(app) + .get(`/workspace/${createdWorkspaceId}`) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address); + + assert.equal(res.status, 200); + assert.equal(res.body.id, createdWorkspaceId); + assert.ok(res.body.owner); + assert.ok(res.body.members); + }); + + await t.test('PUT /workspace should update workspace', async () => { + const updatedName = `Updated Workspace ${Date.now()}`; + + const res = await request(app) + .put('/workspace') + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address) + .set('workspaceid', createdWorkspaceId) + .send({ + name: updatedName, + }); + + assert.equal(res.status, 200); + assert.equal(res.body.name, updatedName); + }); + + await t.test( + 'POST /workspace/members/:member/include should add member to workspace', + async () => { + const res = await request(app) + .post(`/workspace/members/${users[1].id}/include`) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address) + .set('workspaceid', createdWorkspaceId); + + assert.equal(res.status, 200); + + const memberIds = res.body.members.map(m => m.id); + assert.ok(memberIds.includes(users[1].id)); + }, + ); + + await t.test( + 'PUT /workspace/permissions/:member should update member permissions', + async () => { + const newPermissions = { + [PermissionRoles.VIEWER]: ['*'], + [PermissionRoles.ADMIN]: [], + }; + + const res = await request(app) + .put(`/workspace/permissions/${users[1].id}`) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address) + .set('workspaceid', createdWorkspaceId) + .send({ permissions: newPermissions }); + + assert.equal(res.status, 200); + assert.ok(res.body.permissions); + assert.ok(res.body.permissions[users[1].id]); + }, + ); + + await t.test( + 'PUT /workspace/permissions/:member should not allow owner to change own permissions', + async () => { + const newPermissions = { + [PermissionRoles.VIEWER]: ['*'], + }; + + const res = await request(app) + .put(`/workspace/permissions/${users[0].id}`) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address) + .set('workspaceid', createdWorkspaceId) + .send({ permissions: newPermissions }); + + assert.equal(res.status, 401); + }, + ); + + await t.test( + 'POST /workspace/members/:member/remove should remove member from workspace', + async () => { + const res = await request(app) + .post(`/workspace/members/${users[1].id}/remove`) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address) + .set('workspaceid', createdWorkspaceId); + + assert.equal(res.status, 200); + + const memberIds = res.body.members.map(m => m.id); + assert.ok(!memberIds.includes(users[1].id)); + }, + ); + + await t.test( + 'POST /workspace/members/:member/remove should not allow removing owner', + async () => { + const res = await request(app) + .post(`/workspace/members/${users[0].id}/remove`) + .set('Authorization', users[0].token) + .set('signeraddress', users[0].payload.address) + .set('workspaceid', createdWorkspaceId); + + assert.equal(res.status, 401); + }, + ); +}); diff --git a/packages/api/src/utils/FuelProvider.ts b/packages/api/src/utils/FuelProvider.ts index 87d04bfb0..64a149054 100644 --- a/packages/api/src/utils/FuelProvider.ts +++ b/packages/api/src/utils/FuelProvider.ts @@ -1,19 +1,37 @@ import { Provider, ProviderOptions } from 'fuels'; +import { logger } from '@src/config/logger'; +import { ProviderWithCache } from './ProviderWithCache'; +import { cacheConfig } from '@src/config/cache'; const REFRESH_TIME = 60000 * 60; // 60 minutes +const CACHE_CLEAR_INTERVAL = 60000 * 5; // 5 minutes - clear internal SDK caches const FUEL_PROVIDER = process.env.FUEL_PROVIDER || 'https://testnet.fuel.network/v1/graphql'; +// Provider options - use default Fuel SDK settings +// Internal caches are cleared periodically to free memory +const PROVIDER_OPTIONS: ProviderOptions = {}; + export class FuelProvider { private static instance?: FuelProvider; - private providers: Record; - private intervalRef?: NodeJS.Timeout; + private providers: Record; + private chainIdCache: Map; // Global cache for chainIds by URL + private resetIntervalRef?: NodeJS.Timeout; + private cacheCleanIntervalRef?: NodeJS.Timeout; private constructor() { this.providers = {}; + this.chainIdCache = new Map(); } - static async create(url: string, options?: ProviderOptions): Promise { + /** + * Create or get a cached Provider instance + * Returns ProviderWithCache when balance cache is enabled + */ + static async create( + url: string, + options?: ProviderOptions, + ): Promise { if (!FuelProvider.instance) { throw new Error('FuelProvider not started'); } @@ -22,16 +40,113 @@ export class FuelProvider { return FuelProvider.instance.providers[url]; } - const p = new Provider(url, options); - FuelProvider.instance.providers[url] = p; + const mergedOptions = { ...PROVIDER_OPTIONS, ...options }; + + // Use ProviderWithCache when cache is enabled + const provider = cacheConfig.enabled + ? new ProviderWithCache(url, mergedOptions) + : new Provider(url, mergedOptions); + + FuelProvider.instance.providers[url] = provider; + + return provider; + } + + /** + * Get current provider stats + */ + static getStats(): { + size: number; + urls: string[]; + chainIds: Record; + } { + if (!FuelProvider.instance) { + return { size: 0, urls: [], chainIds: {} }; + } + const urls = Object.keys(FuelProvider.instance.providers); + const chainIds = Object.fromEntries(FuelProvider.instance.chainIdCache); + return { + size: urls.length, + urls, + chainIds, + }; + } + + /** + * Get chainId for a URL from global cache + * Fetches from provider if not cached + */ + static async getChainId(url: string): Promise { + if (!FuelProvider.instance) { + throw new Error('FuelProvider not started'); + } + + // Check cache first + const cached = FuelProvider.instance.chainIdCache.get(url); + if (cached !== undefined) { + return cached; + } + + // Get or create provider and fetch chainId + const provider = await FuelProvider.create(url); + const chainId = await provider.getChainId(); + + // Cache the result + FuelProvider.instance.chainIdCache.set(url, chainId); - return p; + return chainId; + } + + /** + * Set chainId in global cache (used by ProviderWithCache) + */ + static setChainId(url: string, chainId: number): void { + if (FuelProvider.instance) { + FuelProvider.instance.chainIdCache.set(url, chainId); + } } async reset(): Promise { - const providers: Record = {}; - providers[FUEL_PROVIDER] = new Provider(FUEL_PROVIDER); - this.providers = providers; + // Clear static caches from Fuel SDK + Provider.clearChainAndNodeCaches(); + + this.providers = {}; + + // Use ProviderWithCache for default provider when cache is enabled + const defaultProvider = cacheConfig.enabled + ? new ProviderWithCache(FUEL_PROVIDER, PROVIDER_OPTIONS) + : new Provider(FUEL_PROVIDER, PROVIDER_OPTIONS); + + this.providers[FUEL_PROVIDER] = defaultProvider; + logger.info('[FuelProvider] Reset - cleared all providers and SDK caches'); + } + + /** + * Clear internal caches of all providers without removing them + * Called periodically to free memory from ResourceCache + */ + clearInternalCaches(): void { + for (const [url, provider] of Object.entries(this.providers)) { + try { + if (provider.cache) { + provider.cache.clear(); + } + } catch (error) { + logger.error({ url, error }, '[FuelProvider] Error clearing cache'); + } + } + // Also clear static SDK caches + Provider.clearChainAndNodeCaches(); + logger.info('[FuelProvider] Cleared internal SDK caches'); + } + + /** + * Manually trigger cache cleanup + */ + static clearCaches(): void { + if (FuelProvider.instance) { + FuelProvider.instance.clearInternalCaches(); + } } static async start(): Promise { @@ -40,15 +155,30 @@ export class FuelProvider { FuelProvider.instance = instance; await instance.reset(); - instance.intervalRef = setInterval(() => { + // Reset providers every 60 minutes + instance.resetIntervalRef = setInterval(() => { instance.reset(); }, REFRESH_TIME); + + // Clear internal caches every 5 minutes + instance.cacheCleanIntervalRef = setInterval(() => { + instance.clearInternalCaches(); + }, CACHE_CLEAR_INTERVAL); + + logger.info( + `[FuelProvider] Started (cache clear every ${ + CACHE_CLEAR_INTERVAL / 60000 + }min)`, + ); } } static stop(): void { - if (FuelProvider.instance?.intervalRef) { - clearInterval(FuelProvider.instance.intervalRef); + if (FuelProvider.instance?.resetIntervalRef) { + clearInterval(FuelProvider.instance.resetIntervalRef); + } + if (FuelProvider.instance?.cacheCleanIntervalRef) { + clearInterval(FuelProvider.instance.cacheCleanIntervalRef); } } } diff --git a/packages/api/src/utils/ProviderWithCache.ts b/packages/api/src/utils/ProviderWithCache.ts new file mode 100644 index 000000000..b20900c0c --- /dev/null +++ b/packages/api/src/utils/ProviderWithCache.ts @@ -0,0 +1,180 @@ +import { BalanceCache } from '@src/server/storage/balance'; +import { logger } from '@src/config/logger'; +import { cacheConfig } from '@src/config/cache'; +import { Address, CoinQuantity, Provider, ProviderOptions } from 'fuels'; + +// Type for address parameter (matches Fuel SDK's Provider.getBalances signature) +type AddressInput = string | Address; + +/** + * Convert address to string (handles both string and Address objects) + */ +function toAddressString(address: AddressInput): string { + return typeof address === 'string' ? address : address.toB256(); +} + +/** + * ProviderWithCache - A transparent wrapper around Fuel's Provider + * that adds caching for getBalances() calls + * + * Features: + * - Extends Provider natively (same API) + * - Automatically caches balance results in Redis + * - Lazy loads chainId (fetched once per instance) + * - Fallback to blockchain if cache fails + * - Force refresh method for critical operations + */ +export class ProviderWithCache extends Provider { + private cachedChainId?: number; + private balanceCache?: BalanceCache; + + constructor(url: string, options?: ProviderOptions) { + super(url, options); + } + + /** + * Get the BalanceCache instance (lazy initialization) + */ + private getBalanceCache(): BalanceCache | null { + if (!cacheConfig.enabled) { + return null; + } + + if (!this.balanceCache) { + try { + this.balanceCache = BalanceCache.getInstance(); + } catch { + // BalanceCache not started yet + return null; + } + } + + return this.balanceCache; + } + + /** + * Get chainId with caching (uses global FuelProvider cache) + */ + private async getCachedChainId(): Promise { + if (this.cachedChainId === undefined) { + // Import dynamically to avoid circular dependency + const { FuelProvider } = await import('./FuelProvider'); + + // Try global cache first + const stats = FuelProvider.getStats(); + const globalCached = stats.chainIds[this.url]; + + if (globalCached !== undefined) { + this.cachedChainId = globalCached; + } else { + // Fetch and cache globally + this.cachedChainId = await this.getChainId(); + FuelProvider.setChainId(this.url, this.cachedChainId); + } + } + return this.cachedChainId; + } + + /** + * Override getBalances to add caching + * This is the main method called by Vault.getBalances() + */ + async getBalances(address: AddressInput): Promise<{ balances: CoinQuantity[] }> { + const cache = this.getBalanceCache(); + + // Convert address to string (handles both string and Address objects) + const addressStr = toAddressString(address); + + // If cache is not available, go directly to blockchain + if (!cache) { + return super.getBalances(address); + } + + try { + const chainId = await this.getCachedChainId(); + + // Try to get from cache + const cachedBalances = await cache.get(addressStr, chainId); + + if (cachedBalances) { + return { balances: cachedBalances }; + } + + // Cache miss - fetch from blockchain + const result = await super.getBalances(address); + + // Store in cache (don't await to not block response) + cache.set(addressStr, result.balances, chainId, this.url).catch(error => { + logger.error({ error }, '[ProviderWithCache] Failed to cache balances'); + }); + + return result; + } catch (error) { + logger.error( + { error }, + '[ProviderWithCache] Error, falling back to blockchain', + ); + // Fallback to blockchain on any cache error + return super.getBalances(address); + } + } + + /** + * Force refresh balances from blockchain (bypass cache) + * Use this for critical operations like before sending transactions + */ + async getBalancesForceRefresh( + address: AddressInput, + ): Promise<{ balances: CoinQuantity[] }> { + const result = await super.getBalances(address); + + // Convert address to string (handles both string and Address objects) + const addressStr = toAddressString(address); + + // Update cache with fresh data + const cache = this.getBalanceCache(); + if (cache) { + try { + const chainId = await this.getCachedChainId(); + await cache.set(addressStr, result.balances, chainId, this.url); + } catch (error) { + logger.error({ error }, '[ProviderWithCache] Failed to update cache'); + } + } + + return result; + } + + /** + * Get balances directly from blockchain without using or modifying cache + * Use this for diagnostic purposes to compare blockchain state with cached state + */ + async getBalancesFromBlockchain( + address: AddressInput, + ): Promise<{ balances: CoinQuantity[] }> { + return super.getBalances(address); + } + + /** + * Invalidate cache for a specific address + */ + async invalidateCache(address: string, chainId?: number): Promise { + const cache = this.getBalanceCache(); + if (cache) { + const resolvedChainId = chainId ?? (await this.getCachedChainId()); + await cache.invalidate(address, resolvedChainId); + } + } + + /** + * Static factory method that creates a connected ProviderWithCache + */ + static async createWithCache( + url: string, + options?: ProviderOptions, + ): Promise { + const provider = new ProviderWithCache(url, options); + + return provider; + } +} diff --git a/packages/api/src/utils/assets.ts b/packages/api/src/utils/assets.ts index cc0dc002c..4627d1ce9 100644 --- a/packages/api/src/utils/assets.ts +++ b/packages/api/src/utils/assets.ts @@ -1,6 +1,9 @@ import { IQuote } from '@src/server/storage'; +import { logger } from '@src/config/logger'; import { fetchFuelAssets } from '@src/server/storage/fuelAssetsFetcher'; import { Assets, NetworkFuel } from 'fuels'; +import { RedisReadClient } from './redis/RedisReadClient'; +import { RedisWriteClient } from './redis/RedisWriteClient'; export type IAsset = { symbol: string; @@ -32,6 +35,27 @@ export type Asset = { const blocklist = ['rsteth', 'rsusde', 're7lrt', 'amphreth']; +// Redis key for assets cache +const ASSETS_CACHE_KEY = 'assets:maps'; +const ASSETS_CACHE_TTL = 60 * 60; // 1 hour + +// Interface for data stored in Redis (without functions) +interface AssetsMapsData { + fuelAssetsList: Assets; + QuotesMock: IQuote[]; + assets: IAsset[]; + assetsMapById: IAssetMapById; + assetsMapBySymbol: IAssetMapBySymbol; +} + +// Interface returned to callers (with functions) +interface AssetsMapsCache extends AssetsMapsData { + fuelUnitAssets: (chainId: number, assetId: string) => number; +} + +// Local reference to fuelAssetsList for the function (lightweight) +let localFuelAssetsList: Assets | null = null; + export const fuelAssetsByChainId = ( chainId: number, fuelAssetsList: Assets, @@ -73,82 +97,129 @@ export const handleFuelUnitAssets = ( return result; }; -export const getAssetsMaps = async () => { - const fuelAssetsList = await fetchFuelAssets(); +// Parse fuel assets filtrando blocklist +const parseFuelAssets = (fuelAssetsList: Assets): Asset[] => { + return fuelAssetsList.reduce((acc, asset) => { + if (blocklist.includes(asset.name.toLocaleLowerCase())) return acc; + + asset.networks + .filter(network => network && 'assetId' in network && network.type === 'fuel') + .forEach((network: NetworkFuel) => + acc.push({ + name: asset.name, + slug: asset.name, + assetId: network.assetId, + icon: asset.icon, + units: network.decimals, + }), + ); - const fuelUnitAssets = (chainId: number, assetId: string): number => - handleFuelUnitAssets(fuelAssetsList, chainId, assetId); + return acc; + }, []); +}; + +// Converte assets para formato simplificado +const parseAssetsToSimpleFormat = (fuelAssets: Asset[]): IAsset[] => { + return fuelAssets.map(asset => ({ + symbol: asset.slug, + id: asset.assetId, + })); +}; + +// Cria map de assets por ID +const createAssetsMapById = (fuelAssets: Asset[]): IAssetMapById => { + return fuelAssets.reduce((previousValue, currentValue) => { + return { + ...previousValue, + [currentValue.assetId]: { + symbol: currentValue.slug, + slug: currentValue.slug, + }, + }; + }, {}); +}; + +// Cria map de assets por símbolo +const createAssetsMapBySymbol = (fuelAssets: Asset[]): IAssetMapBySymbol => { + return fuelAssets.reduce((previousValue, currentValue) => { + return { + ...previousValue, + [currentValue.slug]: { + slug: currentValue.slug, + id: currentValue.assetId, + }, + }; + }, {}); +}; - const fuelAssets = (): Asset[] => - fuelAssetsList.reduce((acc, asset) => { - if (blocklist.includes(asset.name.toLocaleLowerCase())) return acc; - - asset.networks - .filter( - network => network && 'assetId' in network && network.type === 'fuel', - ) - .forEach((network: NetworkFuel) => - acc.push({ - name: asset.name, - slug: asset.name, - assetId: network.assetId, - icon: asset.icon, - units: network.decimals, - }), - ); - - return acc; - }, []); - - const assets = fuelAssets().map(asset => { +// Gera quotes mock para desenvolvimento +const generateQuotesMock = (assetsMapBySymbol: IAssetMapBySymbol): IQuote[] => { + return Object.entries(assetsMapBySymbol).map(([key, value]) => { + const price = Math.random() * 100; return { - symbol: asset.slug, - id: asset.assetId, + assetId: value.id, + price, }; }); +}; + +export const getAssetsMaps = async (): Promise => { + // Try to get from Redis first + try { + const cached = await RedisReadClient.get(ASSETS_CACHE_KEY); + if (cached) { + const data: AssetsMapsData = JSON.parse(cached); + // Store locally for the function + localFuelAssetsList = data.fuelAssetsList; + + const fuelUnitAssets = (chainId: number, assetId: string): number => + handleFuelUnitAssets(data.fuelAssetsList, chainId, assetId); + + return { ...data, fuelUnitAssets }; + } + } catch (error) { + logger.error({ error }, '[AssetsCache] Error reading from Redis'); + } + + // Cache miss - fetch from source + const fuelAssetsList = await fetchFuelAssets(); + localFuelAssetsList = fuelAssetsList; + + const fuelUnitAssets = (chainId: number, assetId: string): number => + handleFuelUnitAssets(fuelAssetsList, chainId, assetId); + + // Parse dos assets usando funções separadas + const fuelAssets = parseFuelAssets(fuelAssetsList); + const assets = parseAssetsToSimpleFormat(fuelAssets); + const assetsMapById = createAssetsMapById(fuelAssets); + const assetsMapBySymbol = createAssetsMapBySymbol(fuelAssets); + const QuotesMock = generateQuotesMock(assetsMapBySymbol); - const assetsMapById: IAssetMapById = fuelAssets().reduce( - (previousValue, currentValue) => { - return { - ...previousValue, - [currentValue.assetId]: { - symbol: currentValue.slug, - slug: currentValue.slug, - }, - }; - }, - {}, - ); - - const assetsMapBySymbol: IAssetMapBySymbol = fuelAssets().reduce( - (previousValue, currentValue) => { - return { - ...previousValue, - [currentValue.slug]: { - slug: currentValue.slug, - id: currentValue.assetId, - }, - }; - }, - {}, - ); - - const QuotesMock: IQuote[] = Object.entries(assetsMapBySymbol).map( - ([key, value]) => { - const price = Math.random() * 100; - return { - assetId: value.id, - price, - }; - }, - ); - - return { + // Data to store in Redis (without functions) + const dataToCache: AssetsMapsData = { fuelAssetsList, QuotesMock, assets, assetsMapById, assetsMapBySymbol, - fuelUnitAssets, }; + + // Store in Redis (async, don't block) + RedisWriteClient.setWithTTL( + ASSETS_CACHE_KEY, + JSON.stringify(dataToCache), + ASSETS_CACHE_TTL, + ).catch(error => logger.error({ error }, '[AssetsCache] Error writing to Redis')); + + return { ...dataToCache, fuelUnitAssets }; +}; + +// Função para limpar o cache (útil para testes ou refresh manual) +export const clearAssetsMapsCache = async () => { + localFuelAssetsList = null; + try { + await RedisWriteClient.del([ASSETS_CACHE_KEY]); + } catch (error) { + logger.error({ error }, '[AssetsCache] Error clearing Redis cache'); + } }; diff --git a/packages/api/src/utils/balance.ts b/packages/api/src/utils/balance.ts index 7cb06eab8..ac983c102 100644 --- a/packages/api/src/utils/balance.ts +++ b/packages/api/src/utils/balance.ts @@ -4,7 +4,6 @@ import App from '@src/server/app'; import { Transaction } from '@src/models'; import { isOutputCoin } from './outputTypeValidate'; import { getAssetsMaps } from './assets'; -import { tokensIDS } from './assets-token/addresses'; const { FUEL_PROVIDER_CHAIN_ID } = process.env; @@ -41,8 +40,6 @@ const calculateBalanceUSD = async ( const quotes = await App.getInstance()._quoteCache.getActiveQuotes(); for (const balance of balances ?? []) { - let priceUSD = 0; - const units = fuelUnitAssets(chainId, balance.assetId); const formattedAmount = balance.amount .format({ @@ -50,19 +47,7 @@ const calculateBalanceUSD = async ( }) .replace(/,/g, ''); - if (balance.assetId === tokensIDS.stFUEL) { - priceUSD = quotes[tokensIDS.FUEL]; - - const amountFuel = await convertStFuelToFuel(formattedAmount); - - balanceUSD += amountFuel * priceUSD; - continue; - } - - if (quotes[balance.assetId]) { - priceUSD = quotes[balance.assetId]; - } - + const priceUSD = quotes[balance.assetId] ?? 0; balanceUSD += parseFloat(formattedAmount) * priceUSD; } @@ -91,13 +76,39 @@ const subCoins = ( .filter(balance => balance.amount.gt(bn.parseUnits('0'))); }; -const convertStFuelToFuel = async (balance: string): Promise => { - const DECIMALS = 10 ** 9; - const rigInstance = await App.getInstance()._rigCache; - const price = (await rigInstance.getRatio()) / DECIMALS; - const totalStFuelToken = Number(balance || '0'); +/** + * Compare two arrays of CoinQuantity to detect balance changes + * @param cached - Previously cached balances + * @param current - Current balances from blockchain + * @returns true if balances are different, false if identical + */ +const compareBalances = ( + cached: CoinQuantity[], + current: CoinQuantity[], +): boolean => { + if (cached.length !== current.length) { + return true; + } + + // Sort by assetId for comparison + const sortFn = (a: CoinQuantity, b: CoinQuantity) => + a.assetId.localeCompare(b.assetId); + const cachedSorted = [...cached].sort(sortFn); + const currentSorted = [...current].sort(sortFn); + + for (let i = 0; i < cachedSorted.length; i++) { + const cachedAsset = cachedSorted[i]; + const currentAsset = currentSorted[i]; + + if ( + cachedAsset.assetId !== currentAsset.assetId || + !cachedAsset.amount.eq(currentAsset.amount) + ) { + return true; + } + } - return totalStFuelToken / price; + return false; }; -export { calculateReservedCoins, calculateBalanceUSD, subCoins }; +export { calculateReservedCoins, calculateBalanceUSD, subCoins, compareBalances }; diff --git a/packages/api/src/utils/discord.ts b/packages/api/src/utils/discord.ts index 88ea63b65..a11645b3a 100644 --- a/packages/api/src/utils/discord.ts +++ b/packages/api/src/utils/discord.ts @@ -1,3 +1,4 @@ +import { logger } from '@src/config/logger'; import axios from 'axios'; import { format } from 'date-fns'; @@ -72,7 +73,7 @@ class DiscordUtils { }); } } catch (e) { - console.log(`[DISCORD] Error on send message: `, e); + logger.error({ error: e }, '[DISCORD] Error on send message'); } } } diff --git a/packages/api/src/utils/error/index.ts b/packages/api/src/utils/error/index.ts index 4a962ee4d..6faae7d86 100644 --- a/packages/api/src/utils/error/index.ts +++ b/packages/api/src/utils/error/index.ts @@ -1,17 +1,19 @@ +import { logger } from '@src/config/logger'; import GeneralError from '@utils/error/GeneralError'; export { default as BadRequest } from './BadRequest'; +export * from './Forbidden'; export * from './GeneralError'; +export { default as Internal } from './Internal'; export { default as NotFound } from './NotFound'; -export * from './Forbidden'; export * from './Unauthorized'; -export { default as Internal } from './Internal'; export enum Responses { BadRequest = 400, Unauthorized = 401, Forbidden = 403, NotFound = 404, + Internal = 500, } export interface ErrorResponse { @@ -19,11 +21,11 @@ export interface ErrorResponse { statusCode: Responses; } -const error = ( +const error = ( payload: ResponsePayload, - statusCode: Responses, + statusCode: Responses = Responses.Internal, ): ErrorResponse => { - console.log(`[ERROR]`, payload); + logger.error({ payload }, '[ERROR]'); if (payload && 'detail' in payload && process.env.NODE_ENV !== 'development') { payload.detail = null; } diff --git a/packages/api/src/utils/extractPredicatesFromTransaction.ts b/packages/api/src/utils/extractPredicatesFromTransaction.ts new file mode 100644 index 000000000..c54687cd9 --- /dev/null +++ b/packages/api/src/utils/extractPredicatesFromTransaction.ts @@ -0,0 +1,31 @@ +import { Transaction } from '@src/models'; + +/** + * Extract all predicate addresses involved in a transaction from its summary + * @param transaction - The transaction object + * @returns Array of unique predicate addresses + */ +export const extractPredicatesFromTransaction = ( + transaction: Transaction, +): string[] => { + const addresses = new Set(); + + // Add the main predicate address + if (transaction.predicate?.predicateAddress) { + addresses.add(transaction.predicate.predicateAddress); + } + + // Extract addresses from operations in summary + if ( + transaction.summary?.operations && + Array.isArray(transaction.summary.operations) + ) { + for (const operation of transaction.summary.operations) { + if (operation.to?.address) { + addresses.add(operation.to.address); + } + } + } + + return Array.from(addresses); +}; diff --git a/packages/api/src/utils/formatAssets.ts b/packages/api/src/utils/formatAssets.ts index 2fa60099b..1accf80e6 100644 --- a/packages/api/src/utils/formatAssets.ts +++ b/packages/api/src/utils/formatAssets.ts @@ -1,4 +1,7 @@ +import { ASSETS, FIAT_CURRENCIES } from '@src/constants/assets'; +import { Transaction, TransactionTypeWithRamp } from '@src/models'; import { + bn, BN, Operation, OperationName, @@ -13,6 +16,7 @@ export type AssetFormat = { assetId: string; amount: string; to: string; + currency?: string; }; const formatAssets = ( @@ -93,4 +97,56 @@ const formatAssetFromOperations = ( return assets; }; -export { formatAssetFromOperations, formatAssets }; +const formatAssetFromRampTransaction = ( + transaction: Transaction, +): AssetFormat[] => { + if (!transaction.rampTransaction) return []; + + const { + destinationAmount, + sourceAmount, + destinationCurrency, + sourceCurrency, + providerData, + } = transaction.rampTransaction; + + const isOnRamp = transaction.type === TransactionTypeWithRamp.ON_RAMP_DEPOSIT; + + // BRL amounts from Meld come with comma as decimal separator + // We need to replace it with a dot to parse it correctly + const formattedSourceAmount = + isOnRamp && sourceCurrency === 'BRL' + ? sourceAmount.replace(',', '.') + : sourceAmount; + + const formattedDestinationAmount = + !isOnRamp && destinationCurrency === 'BRL' + ? destinationAmount.replace(',', '.') + : destinationAmount; + + return [ + // source currency + { + amount: bn.parseUnits(formattedSourceAmount).toString('hex'), + assetId: isOnRamp ? FIAT_CURRENCIES[sourceCurrency] || '' : ASSETS.FUEL_ETH, + to: isOnRamp + ? providerData?.transactionData?.cryptoDetails.sourceWalletAddress || '' + : transaction.predicate.predicateAddress, + currency: sourceCurrency, + }, + // destination currency + { + assetId: isOnRamp + ? ASSETS.FUEL_ETH + : FIAT_CURRENCIES[destinationCurrency] || '', + amount: bn.parseUnits(formattedDestinationAmount).toString('hex'), + to: isOnRamp + ? transaction.predicate.predicateAddress + : providerData?.transactionData?.cryptoDetails.destinationWalletAddress || + '', + currency: destinationCurrency, + }, + ]; +}; + +export { formatAssetFromOperations, formatAssetFromRampTransaction, formatAssets }; diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 985f8b7f9..5a0f8ae49 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -15,3 +15,6 @@ export * from './formatAssets'; export * from './redis/RedisReadClient'; export * from './redis/RedisWriteClient'; export * from './FuelProvider'; +export * from './ProviderWithCache'; +export * from './extractPredicatesFromTransaction'; +export * from './processBatch'; diff --git a/packages/api/src/utils/processBatch.ts b/packages/api/src/utils/processBatch.ts new file mode 100644 index 000000000..110d6ce3c --- /dev/null +++ b/packages/api/src/utils/processBatch.ts @@ -0,0 +1,31 @@ +/** + * Process items in batches with concurrency control + * Prevents overwhelming external services with too many parallel requests + * + * @param items Items to process + * @param batchSize Number of items to process in parallel per batch + * @param processor Async function to process each item + * @returns Array of results maintaining order + * + * @example + * ```ts + * const users = [user1, user2, ..., user20]; + * const results = await processBatch(users, 5, async (user) => { + * return await fetchUserData(user.id); + * }); + * // Processes 5 users at a time, maintains original order + * ``` + */ +export async function processBatch( + items: T[], + batchSize: number, + processor: (item: T) => Promise, +): Promise { + const results: R[] = []; + for (let i = 0; i < items.length; i += batchSize) { + const batch = items.slice(i, i + batchSize); + const batchResults = await Promise.all(batch.map(processor)); + results.push(...batchResults); + } + return results; +} diff --git a/packages/api/src/utils/redis/RedisReadClient.ts b/packages/api/src/utils/redis/RedisReadClient.ts index a00bf6b91..0f0b2b9fb 100644 --- a/packages/api/src/utils/redis/RedisReadClient.ts +++ b/packages/api/src/utils/redis/RedisReadClient.ts @@ -1,4 +1,5 @@ import { RedisClientType, createClient } from 'redis'; +import { logger } from '@src/config/logger'; import { RedisMockStore } from './redis-test-mock'; const REDIS_URL_READ = process.env.REDIS_URL_WRITE || 'redis://127.0.0.1:6379'; @@ -12,7 +13,7 @@ export class RedisReadClient { static async start() { if (RedisReadClient.isMock) { - console.log('[RedisReadClient] Rodando com mock em memória'); + logger.info('[RedisReadClient] Running with mock in memory'); RedisReadClient.client = (RedisMockStore as unknown) as RedisClientType; return; } @@ -23,7 +24,7 @@ export class RedisReadClient { try { await RedisReadClient.client.connect(); } catch (e) { - console.error('[REDIS WRITE CONNECT ERROR]', e); + logger.error({ error: e }, '[REDIS WRITE CONNECT ERROR]'); process.exit(1); } } @@ -36,7 +37,7 @@ export class RedisReadClient { try { await RedisReadClient.client.disconnect(); } catch (e) { - console.error('[REDIS READ DISCONNECT ERROR]', e); + logger.error({ error: e }, '[REDIS READ DISCONNECT ERROR]'); process.exit(1); } } @@ -48,7 +49,7 @@ export class RedisReadClient { ? await RedisMockStore.get(key) : await RedisReadClient.client.get(key); } catch (e) { - console.error('[CACHE_SESSIONS_GET_ERROR]', e, key); + logger.error({ error: e, key }, '[CACHE_SESSIONS_GET_ERROR]'); } } @@ -58,7 +59,7 @@ export class RedisReadClient { ? await RedisMockStore.hGetAll(key) : await RedisReadClient.client.hGetAll(key); } catch (e) { - console.error('[CACHE_SESSIONS_GET_ERROR]', e, key); + logger.error({ error: e, key }, '[CACHE_SESSIONS_GET_ERROR]'); } } @@ -84,11 +85,74 @@ export class RedisReadClient { return result; } + /** + * Get all keys matching a pattern (for debugging/stats) + */ + static async keys(pattern: string): Promise { + try { + if (RedisReadClient.isMock) { + const result = await RedisMockStore.scan(pattern); + return Array.from(result.keys()); + } + + const allKeys: string[] = []; + let cursor = 0; + + do { + const result = await RedisReadClient.client.scan(cursor, { + MATCH: pattern, + COUNT: 100, + }); + cursor = result.cursor; + allKeys.push(...result.keys); + } while (cursor !== 0); + + return allKeys; + } catch (e) { + logger.error({ error: e, pattern }, '[CACHE_KEYS_ERROR]'); + return []; + } + } + + /** + * Get TTL of a key in seconds + */ + static async ttl(key: string): Promise { + try { + if (RedisReadClient.isMock) { + return -1; // Mock doesn't support TTL + } + + return await RedisReadClient.client.ttl(key); + } catch (e) { + logger.error({ error: e, key }, '[CACHE_TTL_ERROR]'); + return -1; + } + } + + /** + * Check if a key exists in Redis + */ + static async exists(key: string): Promise { + try { + if (RedisReadClient.isMock) { + const value = await RedisMockStore.get(key); + return value !== null && value !== undefined; + } + + const result = await RedisReadClient.client.exists(key); + return result === 1; + } catch (e) { + logger.error({ error: e, key }, '[CACHE_EXISTS_ERROR]'); + return false; + } + } + static async ping(): Promise { try { return RedisReadClient.isMock ? 'PONG' : await RedisReadClient.client.ping(); } catch (e) { - console.error('[REDIS_READ_PING_ERROR]', e); + logger.error({ error: e }, '[REDIS_READ_PING_ERROR]'); throw e; } } diff --git a/packages/api/src/utils/redis/RedisWriteClient.ts b/packages/api/src/utils/redis/RedisWriteClient.ts index 4dfd4e6a2..da5929370 100644 --- a/packages/api/src/utils/redis/RedisWriteClient.ts +++ b/packages/api/src/utils/redis/RedisWriteClient.ts @@ -1,4 +1,5 @@ import { RedisClientType, createClient } from 'redis'; +import { logger } from '@src/config/logger'; import { RedisMockStore } from './redis-test-mock'; const REDIS_URL_WRITE = process.env.REDIS_URL_WRITE || 'redis://127.0.0.1:6379'; @@ -22,7 +23,7 @@ export class RedisWriteClient { try { await RedisWriteClient.client.connect(); } catch (e) { - console.error('[REDIS WRITE CONNECT ERROR]', e); + logger.error({ error: e }, '[REDIS WRITE CONNECT ERROR]'); process.exit(1); } } @@ -35,7 +36,7 @@ export class RedisWriteClient { try { await RedisWriteClient.client.disconnect(); } catch (e) { - console.error('[REDIS WRITE DISCONNECT ERROR]', e); + logger.error({ error: e }, '[REDIS WRITE DISCONNECT ERROR]'); process.exit(1); } } @@ -51,7 +52,7 @@ export class RedisWriteClient { EX: 60 * 40, // 40 min }); } catch (e) { - console.error('[CACHE_SET_ERROR]', e, key, value); + logger.error({ error: e, key, value }, '[CACHE_SET_ERROR]'); } } @@ -74,7 +75,7 @@ export class RedisWriteClient { await RedisWriteClient.client.hSet(key, fields.flat()); await RedisWriteClient.client.expire(key, 60 * 40); // 40 min } catch (e) { - console.error('[CACHE_HMSET_ERROR]', e, key, values); + logger.error({ error: e, key, values }, '[CACHE_HMSET_ERROR]'); } } @@ -89,7 +90,60 @@ export class RedisWriteClient { await RedisWriteClient.client.del(keys); } catch (e) { - console.error('[CACHE_SESSIONS_REMOVE_ERROR]', e, keys); + logger.error({ error: e, keys }, '[CACHE_SESSIONS_REMOVE_ERROR]'); + } + } + + /** + * Set a key with custom TTL (in seconds) + */ + static async setWithTTL(key: string, value: string | number, ttlSeconds: number) { + try { + if (RedisWriteClient.isMock) { + return RedisMockStore.set(key, String(value)); + } + + await RedisWriteClient.client.set(key, value, { + EX: ttlSeconds, + }); + } catch (e) { + logger.error({ error: e, key }, '[CACHE_SET_WITH_TTL_ERROR]'); + } + } + + /** + * Delete keys matching a pattern using SCAN + */ + static async delByPattern(pattern: string): Promise { + try { + if (RedisWriteClient.isMock) { + const keys = await RedisMockStore.scan(pattern); + for (const key of keys.keys()) { + await RedisMockStore.set(key, undefined); + } + return keys.size; + } + + let deletedCount = 0; + let cursor = 0; + + do { + const result = await RedisWriteClient.client.scan(cursor, { + MATCH: pattern, + COUNT: 100, + }); + cursor = result.cursor; + + if (result.keys.length > 0) { + await RedisWriteClient.client.del(result.keys); + deletedCount += result.keys.length; + } + } while (cursor !== 0); + + return deletedCount; + } catch (e) { + logger.error({ error: e, pattern }, '[CACHE_DEL_BY_PATTERN_ERROR]'); + return 0; } } @@ -99,7 +153,7 @@ export class RedisWriteClient { ? 'PONG' : await RedisWriteClient.client.ping(); } catch (e) { - console.error('[REDIS_WRITE_PING_ERROR]', e); + logger.error({ error: e }, '[REDIS_WRITE_PING_ERROR]'); throw e; } } diff --git a/packages/api/src/utils/runMode.ts b/packages/api/src/utils/runMode.ts index b383a6c94..8801479fa 100644 --- a/packages/api/src/utils/runMode.ts +++ b/packages/api/src/utils/runMode.ts @@ -1,5 +1,4 @@ - - -export const isDevMode = process.env.NODE_ENV === 'development' -|| process.env.NODE_ENV === 'test' -|| process.env.API_ENVIROMENT === 'development'; \ No newline at end of file +export const isDevMode = + process.env.NODE_ENV === 'development' || + process.env.NODE_ENV === 'test' || + process.env.API_ENVIROMENT === 'development'; diff --git a/packages/api/src/utils/toCamelCase.ts b/packages/api/src/utils/toCamelCase.ts new file mode 100644 index 000000000..e0c952493 --- /dev/null +++ b/packages/api/src/utils/toCamelCase.ts @@ -0,0 +1,16 @@ +function toCamelCase(key: string): string { + return key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); +} + +export function keysToCamel(input: any): T { + if (Array.isArray(input)) { + return input.map(item => keysToCamel(item)) as any; + } else if (input !== null && typeof input === 'object') { + return Object.keys(input).reduce((acc, key) => { + const camelKey = toCamelCase(key); + (acc as any)[camelKey] = keysToCamel(input[key]); + return acc; + }, {} as T); + } + return input; +} diff --git a/packages/api/src/utils/token/utils.ts b/packages/api/src/utils/token/utils.ts index 37bd41bd3..3bce70913 100644 --- a/packages/api/src/utils/token/utils.ts +++ b/packages/api/src/utils/token/utils.ts @@ -1,4 +1,5 @@ import { addMinutes, differenceInMinutes, isPast, parseISO } from 'date-fns'; +import { logger } from '@src/config/logger'; import { Address } from 'fuels'; import { MoreThan } from 'typeorm'; @@ -159,7 +160,6 @@ export class TokenUtils { const provider = await FuelProvider.create(network ?? FUEL_PROVIDER); const _token = await UserToken.findOne({ where: { user_id: userId }, - relations: ['workspace'], }); const _network = { @@ -171,7 +171,7 @@ export class TokenUtils { await _token.save(); await App.getInstance()._sessionCache.updateSession(_token.token); - return true; + return _network; } static async createAuthToken( @@ -190,8 +190,6 @@ export class TokenUtils { order: { createdAt: 'DESC' }, }); - console.log('code1', code); - if (!code) { throw new Unauthorized({ type: ErrorTypes.Unauthorized, @@ -275,7 +273,7 @@ export class TokenUtils { return token; } catch (e) { - console.log('[RENEW TOKEN ERROR]: DATA FORMAT', e); + logger.error({ error: e }, '[RENEW TOKEN ERROR]: DATA FORMAT'); return token; } } diff --git a/packages/api/src/utils/token/web3.ts b/packages/api/src/utils/token/web3.ts index d9c86112f..76d181917 100644 --- a/packages/api/src/utils/token/web3.ts +++ b/packages/api/src/utils/token/web3.ts @@ -1,4 +1,4 @@ -import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; +import { hexToBytes } from '@noble/curves/abstract/utils'; import { secp256r1 } from '@noble/curves/p256'; import { Signer, hashMessage } from 'fuels'; import { @@ -8,7 +8,7 @@ import { pubToAddress, } from '@ethereumjs/util'; -import { TypeUser, User } from '@src/models'; +import { User } from '@src/models'; export const recoverFuelSignature = async (digest: string, signature: string) => { return Signer.recoverAddress(hashMessage(digest), signature).toHexString(); @@ -20,7 +20,7 @@ export const recoverEvmSignature = async (digest: string, signature: string) => const { v, r, s } = fromRpcSig(signature); const pubKey = ecrecover(msgHash, v, r, s); const recoveredAddress = Buffer.from(pubToAddress(pubKey)).toString('hex'); - + return `0x${recoveredAddress}`; }; diff --git a/packages/api/tsconfig.build.json b/packages/api/tsconfig.build.json new file mode 100644 index 000000000..892664987 --- /dev/null +++ b/packages/api/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "src/tests", "src/mocks"] +} diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index b5676b5d5..ee56bf0d0 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -4,6 +4,8 @@ "module": "CommonJS", "outDir": "build", "baseUrl": "./src", + "incremental": true, + "tsBuildInfoFile": "./build/.tsbuildinfo", "emitDecoratorMetadata": true, "experimentalDecorators": true, "resolveJsonModule": true, diff --git a/packages/chain/Makefile b/packages/chain/Makefile index f902bb305..1d56a0542 100644 --- a/packages/chain/Makefile +++ b/packages/chain/Makefile @@ -1,5 +1,14 @@ up: docker compose -p bako-safe_dev --env-file .env.chain up -d --build > /dev/null 2>&1 + @echo "✅ Chain containers started." + +wait: + @echo "⏳ Waiting for fuel-core to be healthy..."; \ + until [ "$$(docker inspect -f '{{.State.Health.Status}}' bakosafe_fuel-core 2>/dev/null)" = "healthy" ]; do \ + sleep 2; \ + echo "Still waiting for fuel-core..."; \ + done; \ + echo "✅ fuel-core is healthy." down: docker compose -p bako-safe_dev stop > /dev/null 2>&1 diff --git a/packages/chain/docker-compose.yml b/packages/chain/docker-compose.yml index 70a243a31..431ff56d2 100644 --- a/packages/chain/docker-compose.yml +++ b/packages/chain/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3" - services: fuel-core: platform: linux/amd64 diff --git a/packages/chain/fuel-core/Dockerfile b/packages/chain/fuel-core/Dockerfile index 989cd59fd..88d7db89b 100644 --- a/packages/chain/fuel-core/Dockerfile +++ b/packages/chain/fuel-core/Dockerfile @@ -5,7 +5,7 @@ # when upgrading fuel-core # We should be supporting always the same fuel-core version as the fuels (ts-sdk) # https://github.com/FuelLabs/fuels-ts/blob/master/internal/fuel-core/VERSION -FROM ghcr.io/fuellabs/fuel-core:v0.38.0 +FROM ghcr.io/fuellabs/fuel-core:v0.43.1 # dependencies ENV DEBIAN_FRONTEND=noninteractive diff --git a/packages/chain/package.json b/packages/chain/package.json index 3cb9ed7ca..3fcaeff5e 100644 --- a/packages/chain/package.json +++ b/packages/chain/package.json @@ -3,8 +3,8 @@ "version": "0.22.0", "license": "MIT", "scripts": { - "chain:dev:start": "make -C ./ env_file=.env.chain up", - "chain:dev:stop": "make -C ./ env_file=.env.chain" + "chain:dev:start": "make -C ./ up && make -C ./ wait", + "chain:dev:stop": "make -C ./ down" }, "devDependencies": { "husky": "5.2.0" diff --git a/packages/database/Makefile b/packages/database/Makefile index cc46be789..e99d3b401 100644 --- a/packages/database/Makefile +++ b/packages/database/Makefile @@ -1,9 +1,23 @@ -database-init: - docker-compose -f docker-compose.yml --env-file .env.example up --build -d > /dev/null 2>&1 +export DOCKER_API_VERSION ?= 1.44 +NETWORK_NAME ?= bako-network + +.PHONY: ensure-network database-init database-down database-wait + +ensure-network: + @echo "🔧 Checking Docker network '$(NETWORK_NAME)'..."; \ + if ! docker network ls --format '{{.Name}}' | grep -q "^$(NETWORK_NAME)$$"; then \ + docker network create $(NETWORK_NAME) >/dev/null; \ + echo "✅ Network '$(NETWORK_NAME)' created."; \ + else \ + echo "✅ Network '$(NETWORK_NAME)' already exists."; \ + fi + +database-init: ensure-network + docker compose -f docker-compose.yml --env-file .env.example up --build -d @echo "✅ Database containers started." database-down: - docker-compose -f docker-compose.yml --env-file .env.example down > /dev/null 2>&1 + docker compose -f docker-compose.yml --env-file .env.example down > /dev/null 2>&1 @echo "🛑 Database containers stopped." database-wait: diff --git a/packages/database/docker-compose.yml b/packages/database/docker-compose.yml index c018895d7..87f7dd219 100644 --- a/packages/database/docker-compose.yml +++ b/packages/database/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: db: container_name: ${DATABASE_ENVIRONMENT} diff --git a/packages/metabase/Makefile b/packages/metabase/Makefile index d0c4a53ee..a409e5a0f 100644 --- a/packages/metabase/Makefile +++ b/packages/metabase/Makefile @@ -1,2 +1,4 @@ +export DOCKER_API_VERSION ?= 1.44 + deploy: - docker-compose -f docker-compose.yml --env-file ${env_file} up --build -d \ No newline at end of file + docker compose -f docker-compose.yml --env-file ${env_file} up --build -d \ No newline at end of file diff --git a/packages/metabase/docker-compose.yml b/packages/metabase/docker-compose.yml index 90bb1a72e..4930f1f52 100644 --- a/packages/metabase/docker-compose.yml +++ b/packages/metabase/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3" - services: db: container_name: "${DATABASE_CONTAINER_NAME}" diff --git a/packages/redis/Makefile b/packages/redis/Makefile index 6dd4738ad..d6bb22331 100644 --- a/packages/redis/Makefile +++ b/packages/redis/Makefile @@ -1,8 +1,10 @@ +export DOCKER_API_VERSION ?= 1.44 + cache-init: - docker-compose -f docker-compose.yml --env-file .env.example up --build -d > /dev/null 2>&1 + docker compose -f docker-compose.yml --env-file .env.example up --build -d > /dev/null 2>&1 cache-down: - docker-compose -f docker-compose.yml --env-file .env.example down > /dev/null 2>&1 + docker compose -f docker-compose.yml --env-file .env.example down > /dev/null 2>&1 cache-wait: container="redis-bako-dev"; \ diff --git a/packages/socket-server/.env.example b/packages/socket-server/.env.example index a1c3128c6..9277c178c 100644 --- a/packages/socket-server/.env.example +++ b/packages/socket-server/.env.example @@ -1,17 +1,19 @@ -#database -# This file is used to set environment variables for the socket server. -# DATABASE_HOST=127.0.0.1 # Use the following variables to configure the database connection. -DATABASE_HOST=db # to local docker extern network +# Database +# Use 127.0.0.1 for local development, 'db' only works inside Docker network +DATABASE_HOST=127.0.0.1 DATABASE_PORT=5432 DATABASE_USERNAME=postgres DATABASE_PASSWORD=postgres DATABASE_NAME=postgres -# server +# Server SOCKET_NAME=bako-socket-server SOCKET_TIMEOUT_DICONNECT=3600000 # 1 hour SOCKET_PORT=3001 -# socket -UI_URL=http://localhost:5173 -API_URL=http://localhost:3333 \ No newline at end of file +# URLs +UI_URL=http://localhost:5174 +API_URL=http://localhost:3333 + +# Environment +NODE_ENV=development diff --git a/packages/socket-server/.eslintrc.js b/packages/socket-server/.eslintrc.js index e12f599dd..709054e28 100644 --- a/packages/socket-server/.eslintrc.js +++ b/packages/socket-server/.eslintrc.js @@ -20,6 +20,7 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': ['off'], // Allow inferred function return type '@typescript-eslint/no-unused-vars': ['off'], // Enable TS no unused var role '@typescript-eslint/no-explicit-any': ['warn'], // Block "any" as a type + 'no-console': 'error', // Disallow all console.* methods - use logger instead }, ignorePatterns: ['node_modules'], } diff --git a/packages/socket-server/Dockerfile b/packages/socket-server/Dockerfile index b8fe08d69..c790b87a7 100644 --- a/packages/socket-server/Dockerfile +++ b/packages/socket-server/Dockerfile @@ -1,21 +1,43 @@ -FROM arm64v8/node:18.18.2-alpine +# syntax=docker/dockerfile:1.4 +FROM node:22-bookworm AS builder -# Create app directory +# Install pnpm globally +RUN npm install -g pnpm + +# Create the application directory WORKDIR /socket -ADD . /socket -# Install pnpm -RUN npm install -g pnpm +# Copy only package.json first (better layer caching) +COPY package.json ./ -# Install system dependencies -RUN apk add --no-cache wget +# Install dependencies with cache mount for pnpm store +RUN --mount=type=cache,id=pnpm-socket,target=/root/.local/share/pnpm/store \ + pnpm install -# Install app dependencies -RUN pnpm install +# Copy source code after dependencies are installed +COPY . . # Build the application RUN pnpm build +# Production stage - smaller final image +FROM node:22-alpine AS production + +# Install pnpm globally +RUN npm install -g pnpm + +WORKDIR /socket + +# Copy package.json +COPY package.json ./ + +# Install only production dependencies with cache mount +RUN --mount=type=cache,id=pnpm-socket,target=/root/.local/share/pnpm/store \ + pnpm install --prod + +# Copy built application from builder stage +COPY --from=builder /socket/build ./build + # Expose application port EXPOSE 3001 diff --git a/packages/socket-server/Makefile b/packages/socket-server/Makefile index d5cf439d8..53ab75657 100644 --- a/packages/socket-server/Makefile +++ b/packages/socket-server/Makefile @@ -1,14 +1,28 @@ +export DOCKER_API_VERSION ?= 1.44 +NETWORK_NAME ?= bako-network + +.PHONY: build socket-init socket-down socket-wait ensure-network + build: pnpm install pnpm run build -socket-init: +ensure-network: + @echo "🔧 Checking Docker network '$(NETWORK_NAME)'..."; \ + if ! docker network ls --format '{{.Name}}' | grep -q "^$(NETWORK_NAME)$$"; then \ + docker network create $(NETWORK_NAME) >/dev/null; \ + echo "✅ Network '$(NETWORK_NAME)' created."; \ + else \ + echo "✅ Network '$(NETWORK_NAME)' already exists."; \ + fi + +socket-init: ensure-network @echo "🚀 Building and starting Socket Server..."; \ - docker-compose -f docker-compose.yml up --build -d + docker compose -f docker-compose.yml up --build -d socket-down: @echo "🧹 Stopping Socket Server..."; \ - docker-compose -f docker-compose.yml down + docker compose -f docker-compose.yml down socket-wait: @echo "⏳ Waiting for bako-socket-server to be healthy..."; \ diff --git a/packages/socket-server/docker-compose.yml b/packages/socket-server/docker-compose.yml index 76cfae763..a600be150 100644 --- a/packages/socket-server/docker-compose.yml +++ b/packages/socket-server/docker-compose.yml @@ -1,45 +1,43 @@ -version: '3.8' - services: - socket-server: - container_name: bako-socket-server - build: - context: . - dockerfile: Dockerfile - working_dir: /socket - ports: - - '3001:3001' - environment: - # DATABASE - - DATABASE_HOST=db - - DATABASE_PORT=5432 - - DATABASE_USERNAME=postgres - - DATABASE_PASSWORD=postgres - - DATABASE_NAME=postgres + socket-server: + container_name: bako-socket-server + build: + context: . + dockerfile: Dockerfile + working_dir: /socket + ports: + - '3001:3001' + environment: + # DATABASE (uses container name in Docker network) + - DATABASE_HOST=postgres + - DATABASE_PORT=5432 + - DATABASE_USERNAME=postgres + - DATABASE_PASSWORD=postgres + - DATABASE_NAME=postgres - # ASSETS - - UI_URL=http://localhost:5173 - - API_URL=http://localhost:3333 + # ASSETS + - UI_URL=http://localhost:5174 + - API_URL=http://localhost:3333 - # SOCKET - - SOCKET_PORT=3001 - - SOCKET_NAME=bako-socket-server - - SOCKET_TIMEOUT_DICONNECT=3600000 + # SOCKET + - SOCKET_PORT=3001 + - SOCKET_NAME=bako-socket-server + - SOCKET_TIMEOUT_DICONNECT=3600000 - # REDIS - - REDIS_URL_READ=redis://localhost:6379 - - REDIS_URL_WRITE=redis://localhost:6379 + # REDIS (uses container name in Docker network) + - REDIS_URL_READ=redis://redis-bako-dev:6379 + - REDIS_URL_WRITE=redis://redis-bako-dev:6379 - restart: always - healthcheck: - test: ['CMD', 'wget', '--spider', '--quiet', 'http://localhost:3001/health'] - interval: 10s - timeout: 5s - retries: 5 + restart: always + healthcheck: + test: ['CMD', 'wget', '--spider', '--quiet', 'http://localhost:3001/health'] + interval: 10s + timeout: 5s + retries: 5 - networks: - - bako-network + networks: + - bako-network networks: - bako-network: - external: true + bako-network: + external: true diff --git a/packages/socket-server/package.json b/packages/socket-server/package.json index 29ad117be..caa949f9f 100644 --- a/packages/socket-server/package.json +++ b/packages/socket-server/package.json @@ -13,13 +13,15 @@ "keywords": [], "dependencies": { "@socket.io/redis-adapter": "^8.3.0", - "axios": "1.5.1", - "bakosafe": "0.2.2", - "express": "4.17.1", + "axios": "1.13.5", + "bakosafe": "0.6.0", + "date-fns": "2.30.0", + "express": "4.21.2", "express-joi-validation": "5.0.0", "fuels": "0.101.3", "ioredis": "^5.7.0", "pg": "8.5.1", + "pino": "9.6.0", "socket.io": "4.7.2", "ts-node": "^10.9.2", "tsconfig-paths": "^3.15.0", @@ -28,11 +30,11 @@ "devDependencies": { "@commitlint/cli": "12.0.1", "@commitlint/config-conventional": "12.0.1", - "@trivago/prettier-plugin-sort-imports": "2.0.2", + "@trivago/prettier-plugin-sort-imports": "4.3.0", "@types/cors": "2.8.10", "@types/express": "4.17.11", "@types/jest": "^29.5.14", - "@types/jsonwebtoken": "9.0.2", + "@types/jsonwebtoken": "9.0.10", "@types/morgan": "1.9.2", "@types/node": "20.6.0", "@types/pg": "^8.15.5", @@ -45,11 +47,12 @@ "husky": "5.2.0", "jest": "29.7.0", "lint-staged": "10.5.4", + "pino-pretty": "11.2.2", "prettier": "2.2.1", "pretty-quick": "3.1.0", "supertest": "6.1.3", "ts-jest": "^29.4.1", - "ts-node-dev": "1.1.6", + "ts-node-dev": "2.0.0", "tscpaths": "0.0.9" } } diff --git a/packages/socket-server/src/config/logger.ts b/packages/socket-server/src/config/logger.ts new file mode 100644 index 000000000..a53b7af3b --- /dev/null +++ b/packages/socket-server/src/config/logger.ts @@ -0,0 +1,105 @@ +import pino from 'pino' + +const { NODE_ENV, LOG_LEVEL } = process.env + +const isDevelopment = NODE_ENV === 'development' + +const pinoConfig: pino.LoggerOptions = { + level: LOG_LEVEL || (isDevelopment ? 'debug' : 'info'), + timestamp: pino.stdTimeFunctions.isoTime, + + // Sensitive data redaction for security compliance + redact: { + paths: [ + // Authentication & Authorization + 'password', + 'passwd', + 'pwd', + 'token', + 'accessToken', + 'refreshToken', + 'apiKey', + 'api_key', + 'apiSecret', + 'secretKey', + 'secret', + 'authorization', + 'auth', + '*.authorization', + '*.auth', + 'headers.authorization', + + // Cryptographic & Private Keys + 'privateKey', + 'private_key', + 'publicKey', + 'public_key', + 'mnemonic', + 'seed', + 'seedPhrase', + 'signature', + 'signedMessage', + + // User Sensitive Data + 'code', + 'recoveryCode', + 'pin', + 'otp', + 'cookie', + 'cookies', + 'credentials', + 'email', + 'phone', + 'phoneNumber', + 'phone_number', + + // Blockchain Specific + 'privateAddress', + 'signer', + 'wallet', + 'walletAddress', + 'predicateAddress', + 'vault.configurable', + 'operationKey', + 'operationData', + 'apiToken', + 'api_token', + 'API_TOKEN', + + // Database & Server + 'databaseUrl', + 'DATABASE_URL', + 'database_url', + 'connectionString', + 'connection_string', + 'redisUrl', + 'REDIS_URL', + 'redis_url', + 'mongoUrl', + 'MONGO_URL', + 'mongo_url', + 'serverApi', + 'server_api', + + // Network & IP (semi-sensitive) + 'ipAddress', + 'ip_address', + ], + remove: true, + }, + + // Pretty printing for development, JSON for production + transport: isDevelopment + ? { + target: 'pino-pretty', + options: { + colorize: true, + singleLine: false, + translateTime: 'SYS:standard', + ignore: 'pid,hostname', + }, + } + : undefined, +} + +export const logger = pino(pinoConfig) diff --git a/packages/socket-server/src/index.ts b/packages/socket-server/src/index.ts index c7a5fb119..f32d5b30f 100644 --- a/packages/socket-server/src/index.ts +++ b/packages/socket-server/src/index.ts @@ -5,6 +5,7 @@ import axios from 'axios' import { DatabaseClass } from '@utils/database' import { setupSocket } from './socket' +import { logger } from '@src/config/logger' const { SOCKET_PORT, SOCKET_TIMEOUT_DICONNECT, SOCKET_NAME, API_URL } = process.env @@ -31,7 +32,7 @@ const startServer = async () => { setupSocket(io, database, api) server.listen(SOCKET_PORT || 3000, () => { - console.log(`Server running on port ${SOCKET_PORT || 3000}`) + logger.info(`Server running on port ${SOCKET_PORT || 3000}`) }) } diff --git a/packages/socket-server/src/modules/switchNetwork.ts b/packages/socket-server/src/modules/switchNetwork.ts index a8ff3d5d1..86d7e34c1 100644 --- a/packages/socket-server/src/modules/switchNetwork.ts +++ b/packages/socket-server/src/modules/switchNetwork.ts @@ -3,6 +3,7 @@ import { Network, SelectNetworkArguments, TransactionRequestLike } from 'fuels' import { Socket } from 'socket.io' import { DatabaseClass } from '@utils/database' import { DappService, PredicateService } from '@src/services' +import { logger } from '@src/config/logger' export interface IEventSwitchNetwork_REQUEST { _network: SelectNetworkArguments @@ -44,14 +45,7 @@ export class SwitchNetworkEventHandler { const { _network } = data const dapp = await this.dappService.getBySessionIdWithPredicate(auth.sessionId) - console.log( - '[DAPP]: ', - JSON.stringify({ - dapp, - origin, - host, - }), - ) + logger.info({ dapp, origin, host }, '[DAPP]') const isValid = dapp && dapp.origin === origin if (!isValid) return @@ -87,7 +81,7 @@ export class SwitchNetworkEventHandler { }, }) } catch (e) { - console.log(e) + logger.error({ error: e }, 'Error in requestSwitchNetwork') } } diff --git a/packages/socket-server/src/modules/transactions.ts b/packages/socket-server/src/modules/transactions.ts index 6b29c37a4..23ab97252 100644 --- a/packages/socket-server/src/modules/transactions.ts +++ b/packages/socket-server/src/modules/transactions.ts @@ -1,4 +1,4 @@ -import { SocketEvents, SocketUsernames } from '@src/types' +import { EFuelConnectorsTypes, SocketEvents, SocketUsernames } from '@src/types' import { BakoProvider, ITransactionSummary, Vault } from 'bakosafe' import crypto from 'crypto' import { Provider, TransactionRequestLike } from 'fuels' @@ -6,6 +6,8 @@ import { Socket } from 'socket.io' import { DatabaseClass } from '@utils/database' import { io, api } from '..' import { DappService, PredicateService, RecoverCodeService, TransactionService } from '@src/services' +import { subMinutes } from 'date-fns' +import { logger } from '@src/config/logger' export interface IEventTX_REQUEST { _transaction: TransactionRequestLike @@ -16,11 +18,17 @@ export interface IEventTX_CREATE { tx: TransactionRequestLike operations: any sign?: boolean + connectorType?: EFuelConnectorsTypes } export interface IEventTX_SIGN { hash: string signedMessage: string + connectorType?: EFuelConnectorsTypes +} + +export interface IEventTX_DELETE { + hash: string } enum IEventTX_STATUS { @@ -66,14 +74,7 @@ export class TransactionEventHandler { const { auth } = socket.handshake const dapp = await this.dappService.getBySessionIdWithPredicate(auth.sessionId) - console.log( - '[DAPP]: ', - JSON.stringify({ - dapp, - origin, - host, - }), - ) + logger.info({ dapp, origin, host }, '[DAPP]') const isValid = dapp && dapp.origin === origin //todo: adicionar emissao de erro @@ -95,13 +96,13 @@ export class TransactionEventHandler { predicateId: vault.id, networkUrl: dapp.network.url, }) - console.log('TX_PENDING', dapp.network.url, tx_pending.count) + logger.info({ url: dapp.network.url, count: tx_pending.count }, 'TX_PENDING') const _provider = dapp.network.url.replace(/^https?:\/\/[^@]+@/, 'https://') const provider = new Provider(_provider) - console.log('VAULT', _provider) + logger.info({ provider: _provider }, 'VAULT') const vaultInstance = new Vault(provider, JSON.parse(vault.configurable), vault.version) const { tx } = await vaultInstance.BakoTransfer(_transaction) @@ -129,11 +130,11 @@ export class TransactionEventHandler { version: vault.version, }, tx, - validAt: code.valid_at, + validAt: subMinutes(new Date(code.valid_at), 1), // Subtracts 1 minute to allow delete tx before expiration }, }) } catch (e) { - console.log(e) + logger.error({ error: e }, 'Error in request transaction') } } @@ -141,7 +142,9 @@ export class TransactionEventHandler { const { sessionId, username, request_id } = socket.handshake.auth const { origin, host } = socket.handshake.headers - const { tx, operations, sign } = data + const { tx, operations, sign, connectorType } = data + + const isEvmOrSocialConnector = connectorType === EFuelConnectorsTypes.EVM || connectorType === EFuelConnectorsTypes.SOCIAL const uiRoom = `${sessionId}:${SocketUsernames.UI}:${request_id}` const connectorRoom = `${sessionId}:${SocketUsernames.CONNECTOR}:${request_id}` @@ -186,7 +189,7 @@ export class TransactionEventHandler { // ------------------------------ [INVALIDATION] ------------------------------ if (!sign) { - await this.recoverCodeService.delete(code.id).catch(error => console.error(error)) + await this.recoverCodeService.delete(code.id).catch(error => logger.error({ error }, 'Failed to delete recovery code')) } // ------------------------------ [EMIT] ------------------------------ @@ -201,21 +204,24 @@ export class TransactionEventHandler { hash: _tx.hashTxId, status: IEventTX_STATUS.SUCCESS, sign, + predicateVersion: vault.predicateVersion, }, }) - // Confirm tx creation to CONNECTOR - socket.to(connectorRoom).emit(SocketEvents.DEFAULT, { - username, - room: sessionId, - request_id, - to: SocketUsernames.CONNECTOR, - type: SocketEvents.TX_CONFIRM, - data: { - id: _tx.hashTxId, - status: IEventTX_STATUS.SUCCESS, - }, - }) + if (!isEvmOrSocialConnector) { + // Confirm tx creation to CONNECTOR + socket.to(connectorRoom).emit(SocketEvents.DEFAULT, { + username, + room: sessionId, + request_id, + to: SocketUsernames.CONNECTOR, + type: SocketEvents.TX_CONFIRM, + data: { + id: _tx.hashTxId, + status: IEventTX_STATUS.SUCCESS, + }, + }) + } } catch (e) { io.to(uiRoom).emit(SocketEvents.DEFAULT, { username, @@ -234,8 +240,12 @@ export class TransactionEventHandler { const { sessionId, username, request_id } = socket.handshake.auth const { origin, host } = socket.handshake.headers - const { hash, signedMessage } = data - const room = `${sessionId}:${SocketUsernames.UI}:${request_id}` + const { hash, signedMessage, connectorType } = data + + const isEvmOrSocialConnector = connectorType === EFuelConnectorsTypes.EVM || connectorType === EFuelConnectorsTypes.SOCIAL + + const uiRoom = `${sessionId}:${SocketUsernames.UI}:${request_id}` + const connectorRoom = `${sessionId}:${SocketUsernames.CONNECTOR}:${request_id}` const { auth } = socket.handshake @@ -269,10 +279,10 @@ export class TransactionEventHandler { ) // ------------------------------ [INVALIDATION] ------------------------------ - await this.recoverCodeService.delete(code.id).catch(error => console.error(error)) + await this.recoverCodeService.delete(code.id).catch(error => logger.error({ error }, 'Failed to delete recovery code')) // ------------------------------ [EMIT] ------------------------------ - io.to(room).emit(SocketEvents.DEFAULT, { + io.to(uiRoom).emit(SocketEvents.DEFAULT, { username, room: sessionId, request_id, @@ -282,8 +292,23 @@ export class TransactionEventHandler { status: IEventTX_STATUS.SUCCESS, }, }) + + if (isEvmOrSocialConnector) { + // Confirm tx creation and sign to CONNECTOR + socket.to(connectorRoom).emit(SocketEvents.DEFAULT, { + username, + room: sessionId, + request_id, + to: SocketUsernames.CONNECTOR, + type: SocketEvents.TX_CONFIRM, + data: { + id: hash, + status: IEventTX_STATUS.SUCCESS, + }, + }) + } } catch (e) { - io.to(room).emit(SocketEvents.DEFAULT, { + io.to(uiRoom).emit(SocketEvents.DEFAULT, { username, room: sessionId, request_id, @@ -295,4 +320,39 @@ export class TransactionEventHandler { }) } } + + async delete({ data, socket }: IEvent) { + const { origin, host } = socket.handshake.headers + + const { hash } = data + + const { auth } = socket.handshake + + try { + if (origin != UI_URL) throw new Error('Invalid origin') + + // ------------------------------ [DAPP] ------------------------------ + const dapp = await this.dappService.getBySessionIdWithPredicate(auth.sessionId) + + if (!dapp) throw new Error('Dapp not found') + + // ------------------------------ [CODE] ------------------------------ + const code = await this.recoverCodeService.getValid({ origin: host, userId: dapp.user_id }) + + if (!code.code) throw new Error('Recover code not found') + + // ---------------------[VALIDATE SIGNATURE] -------------------------- + await api.delete(`/transaction/by-hash/${hash}`, { + headers: { + authorization: code.code, + signeraddress: dapp.user_address, + }, + }) + + // ------------------------- [INVALIDATION] --------------------------- + await this.recoverCodeService.delete(code.id).catch(error => logger.error({ error }, 'Failed to delete recovery code')) + } catch (e) { + logger.error({ error: e }, 'Error in delete transaction') + } + } } diff --git a/packages/socket-server/src/services/recoverCode.ts b/packages/socket-server/src/services/recoverCode.ts index 2c809b486..e4389af2d 100644 --- a/packages/socket-server/src/services/recoverCode.ts +++ b/packages/socket-server/src/services/recoverCode.ts @@ -9,17 +9,20 @@ interface ICreateParams extends IGetValidParams { code: string metadata: string network: string + expTime?: string } +const DEFAULT_EXP_TIME = '3' // minutes + export class RecoverCodeService extends BaseService { - async create({ origin, userId, code, metadata, network }: ICreateParams) { + async create({ origin, userId, code, expTime = DEFAULT_EXP_TIME, metadata, network }: ICreateParams) { return await this.database.query( ` INSERT INTO recover_codes (origin, owner, type, code, valid_at, metadata, used, network) - VALUES ($1, $2, 'AUTH_ONCE', $3, NOW() + INTERVAL '2 minutes', $4, false, $5) + VALUES ($1, $2, 'AUTH_ONCE', $3, NOW() + ($4 || ' minutes')::interval, $5, false, $6) RETURNING *; `, - [origin, userId, code, metadata, network], + [origin, userId, code, expTime, metadata, network], ) } diff --git a/packages/socket-server/src/socket/index.ts b/packages/socket-server/src/socket/index.ts index 1957f6e19..22a0d67ef 100644 --- a/packages/socket-server/src/socket/index.ts +++ b/packages/socket-server/src/socket/index.ts @@ -6,6 +6,7 @@ import { SwitchNetworkEventHandler } from '../modules/switchNetwork' import { SocketEvents, SocketUsernames } from '../types' import { DatabaseClass, retryWithBackoff } from '@src/utils' +import { logger } from '@src/config/logger' // import Redis from 'ioredis' // import { createAdapter } from '@socket.io/redis-adapter' @@ -26,12 +27,12 @@ export const setupSocket = (io: SocketIOServer, database: DatabaseClass, api: Ax const requestId = request_id === undefined ? '' : request_id if (!sessionId || !username) { - console.error('[SOCKET]: missing sessionId or username', socket.handshake.auth) + logger.error({ auth: socket.handshake.auth }, '[SOCKET] missing sessionId or username') return socket.disconnect(true) } const room = `${sessionId}:${username}${requestId && `:${requestId}`}` - console.log('\n[SOCKET]: CONNECTED TO', room, '\n') + logger.info({ room }, '[SOCKET] CONNECTED TO') socket.data.messageQueue = [] await socket.join(room) @@ -56,13 +57,23 @@ export const setupSocket = (io: SocketIOServer, database: DatabaseClass, api: Ax [UI] emite esse evento quando o usuário assina a tx - verifica se o evento veio da origem correta -> BAKO-UI [http://localhost:5174, https://safe.bako.global] - recupera as infos do dapp que está tentando criar a tx pelo sessionId - - usa uma credencial temporária (code) que é utilizada para criar e assinar a tx com o pacote bakosafe + - usa uma credencial temporária (code) que é utilizada para criar, assinar e excluir a tx com o pacote bakosafe - consome endpoint /transaction/sign/:hash para assinar a tx - - invalida credencial temporária (code) que foi utilizada para criar e assinar a tx + - invalida credencial temporária (code) que foi utilizada para criar, assinar e excluir a tx - emite uma mensagem para a [UI] com o resultado da assiantura da tx */ socket.on(SocketEvents.TX_SIGN, data => transactionEventHandler.sign({ data, socket })) + /* + [UI] emite esse evento quando a tx é criada, mas não assinada e o popup é fechado + - verifica se o evento veio da origem correta -> BAKO-UI [http://localhost:5174, https://safe.bako.global] + - recupera as infos do dapp que está tentando criar a tx pelo sessionId + - usa uma credencial temporária (code) que é utilizada para criar, assinar e excluir a tx com o pacote bakosafe + - consome o endpoint DELETE - /transaction/by-hash/:hash para excluir a tx + - invalida credencial temporária (code) que foi utilizada para criar, assinar e excluir a tx + */ + socket.on(SocketEvents.TX_DELETE, data => transactionEventHandler.delete({ data, socket })) + /* [CONNECTOR] emite esse evento quando o usuário quer criar uma transação - recupera as infos do dapp que está tentando criar a tx pelo sessionId @@ -85,7 +96,7 @@ export const setupSocket = (io: SocketIOServer, database: DatabaseClass, api: Ax const connectionStateUrl = `/connections/${sessionId}/state` const { data: connected } = await retryWithBackoff(() => api.get(connectionStateUrl), connectionStateUrl) - console.log('[SOCKET] [CONNECTION_STATE] Connected state for session', sessionId, '->', connected) + logger.info({ sessionId, connected }, '[SOCKET] Connected state for session') io.to(connectorRoom).emit(SocketEvents.CONNECTION_STATE, { username: SocketUsernames.CONNECTOR, @@ -96,11 +107,14 @@ export const setupSocket = (io: SocketIOServer, database: DatabaseClass, api: Ax data: connected, }) } catch (error) { - console.error('[SOCKET] [CONNECTION_STATE] Error:', { - message: error.message, - status: error.response?.status, - url: error.config?.url, - }) + logger.error( + { + message: error.message, + status: error.response?.status, + url: error.config?.url, + }, + '[SOCKET] Error fetching connection state for session', + ) // Returns an error response to the client io.to(connectorRoom).emit(SocketEvents.CONNECTION_STATE, { username: SocketUsernames.CONNECTOR, @@ -136,12 +150,30 @@ export const setupSocket = (io: SocketIOServer, database: DatabaseClass, api: Ax const { sessionId, to } = data const room = `${sessionId}:${to}` const clientsInRoom = io.sockets.adapter.rooms.get(room) || new Set() - console.log('[SOCKET SERVER] [SWITCH_NETWORK] Event: ' + JSON.stringify(data)) + logger.info({ data }, '[SOCKET SERVER] [SWITCH_NETWORK] Event') if (clientsInRoom.size > 0) { socket.to(room).emit(SocketEvents.SWITCH_NETWORK, data) } }) + socket.on(SocketEvents.BALANCE_OUTDATED_USER, data => { + const { sessionId, to } = data + const room = `${sessionId}:${to}` + const clientsInRoom = io.sockets.adapter.rooms.get(room) || new Set() + if (clientsInRoom.size > 0) { + socket.to(room).emit(SocketEvents.BALANCE_OUTDATED_USER, data) + } + }) + + socket.on(SocketEvents.BALANCE_OUTDATED_PREDICATE, data => { + const { sessionId, to } = data + const room = `${sessionId}:${to}` + const clientsInRoom = io.sockets.adapter.rooms.get(room) || new Set() + if (clientsInRoom.size > 0) { + socket.to(room).emit(SocketEvents.BALANCE_OUTDATED_PREDICATE, data) + } + }) + socket.on(SocketEvents.NOTIFICATION, data => { const { sessionId, to } = data const room = `${sessionId}:${to}` diff --git a/packages/socket-server/src/types.ts b/packages/socket-server/src/types.ts index 5162e5f3c..6a7f10426 100644 --- a/packages/socket-server/src/types.ts +++ b/packages/socket-server/src/types.ts @@ -20,6 +20,7 @@ export enum SocketEvents { TX_SIGN = '[TX_EVENT_SIGNED]', TX_CONFIRM = '[TX_EVENT_CONFIRMED]', TX_REQUEST = '[TX_EVENT_REQUESTED]', + TX_DELETE = '[TX_EVENT_DELETED]', CONNECTION_STATE = '[CONNECTION_STATE]', DISCONNECT = '[DISCONNECT]', @@ -29,6 +30,9 @@ export enum SocketEvents { NEW_NOTIFICATION = '[NEW_NOTIFICATION]', TRANSACTION = '[TRANSACTION]', + + BALANCE_OUTDATED_USER = '[BALANCE_OUTDATED_USER]', + BALANCE_OUTDATED_PREDICATE = '[BALANCE_OUTDATED_PREDICATE]', } export enum SocketUsernames { @@ -37,6 +41,12 @@ export enum SocketUsernames { API = '[API]', } +export enum EFuelConnectorsTypes { + BAKO = 'Bako Safe', + EVM = 'Ethereum Wallets', + SOCIAL = 'Social Login', +} + export interface IDefaultMessage { username: string room: string diff --git a/packages/socket-server/src/utils/database.ts b/packages/socket-server/src/utils/database.ts index 5b8194cae..d46ae16f9 100644 --- a/packages/socket-server/src/utils/database.ts +++ b/packages/socket-server/src/utils/database.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line prettier/prettier import { Client, type QueryResult } from 'pg' +import { logger } from '@src/config/logger' const { DATABASE_HOST, @@ -20,7 +21,7 @@ interface ConnectionConfig { }; } -const isLocal = DATABASE_HOST === '127.0.0.1' || DATABASE_HOST === 'db' +const isLocal = DATABASE_HOST === '127.0.0.1' || DATABASE_HOST === 'localhost' || DATABASE_HOST === 'db' || DATABASE_HOST === 'postgres' export const defaultConnection: ConnectionConfig = { user: DATABASE_USERNAME, @@ -59,7 +60,7 @@ export class DatabaseClass { if (rows.length === 1) return rows[0] return rows } catch (error) { - console.error('Erro ao executar a query:', error) + logger.error({ error, query }, 'Error executing query') throw error } } diff --git a/packages/socket-server/src/utils/retryWithBackoff.ts b/packages/socket-server/src/utils/retryWithBackoff.ts index f400e9d96..020b40210 100644 --- a/packages/socket-server/src/utils/retryWithBackoff.ts +++ b/packages/socket-server/src/utils/retryWithBackoff.ts @@ -1,3 +1,5 @@ +import { logger } from '@src/config/logger' + const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) type RetryOptions = { @@ -42,7 +44,7 @@ async function retryWithBackoff(fn: () => Promise, url?: string, { retries const jitter = Math.random() * 100 const delay = baseDelay * Math.pow(2, attempt) + jitter - console.warn(`[RETRY] Attempt ${attempt + 1}/${retries + 1} failed (status ${status})${url ? ` for URL: ${url}` : ''}. Retrying in ${Math.round(delay)}ms...`) + logger.warn({ attempt: attempt + 1, totalRetries: retries + 1, status, url, delayMs: Math.round(delay) }, '[RETRY] Attempt failed, retrying...') await sleep(delay) } diff --git a/packages/worker/.env.example b/packages/worker/.env.example index 6b0534c30..7889d54f7 100644 --- a/packages/worker/.env.example +++ b/packages/worker/.env.example @@ -10,7 +10,7 @@ WORKER_MONGO_HOST=localhost WORKER_MONGO_USERNAME=user WORKER_MONGO_PASSWORD=user WORKER_MONGO_PORT=27017 -WORKER_MONGO_ENVIRONMENT=devevelopment +WORKER_MONGO_ENVIRONMENT=development # Redis WORKER_REDIS_HOST=127.0.0.1 diff --git a/packages/worker/Dockerfile b/packages/worker/Dockerfile index 27d66e996..988cf59c1 100644 --- a/packages/worker/Dockerfile +++ b/packages/worker/Dockerfile @@ -1,24 +1,44 @@ -FROM arm64v8/node:18.18.2-alpine - -# Install dependencies necessary for node-gyp -RUN apk add --no-cache python3 make g++ \ - && npm install -g node-gyp +# syntax=docker/dockerfile:1.4 +FROM node:22-bookworm AS builder +# Bookworm already has python3/make/g++ - no need to install # Install pnpm globally RUN npm install -g pnpm # Create the application directory WORKDIR /api -# Add the application content to the working directory -ADD . /api +# Copy only package.json first (better layer caching) +COPY package.json ./ + +# Install dependencies with cache mount for pnpm store +RUN --mount=type=cache,id=pnpm-worker,target=/root/.local/share/pnpm/store \ + pnpm install -# Install the application dependencies using pnpm -RUN pnpm install +# Copy source code after dependencies are installed +COPY . . # Build the application RUN pnpm build +# Production stage - smaller final image +FROM node:22-alpine AS production + +# Install pnpm globally +RUN npm install -g pnpm + +WORKDIR /api + +# Copy package.json +COPY package.json ./ + +# Install only production dependencies with cache mount +RUN --mount=type=cache,id=pnpm-worker,target=/root/.local/share/pnpm/store \ + pnpm install --prod + +# Copy built application from builder stage +COPY --from=builder /api/dist ./dist + # Expose the application port EXPOSE 3063 diff --git a/packages/worker/docker-compose.yml b/packages/worker/docker-compose.yml index d2bad9840..ca6601b65 100644 --- a/packages/worker/docker-compose.yml +++ b/packages/worker/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3" services: api: container_name: ${WORKER_NAME} diff --git a/packages/worker/package.json b/packages/worker/package.json index 5af11068c..c96a69480 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -11,31 +11,33 @@ "author": "Guilherme Roque", "license": "ISC", "dependencies": { - "@bull-board/api": "^6.5.3", - "@bull-board/express": "^6.5.3", + "@bull-board/api": "^6.12.0", + "@bull-board/express": "^6.12.0", "@envio-dev/hypersync-client": "0.6.2", "@types/bull": "^4.10.4", "@types/node-cron": "3.0.11", - "bull": "^4.16.4", - "express": "4.17.1", + "bull": "^4.16.5", + "express": "4.21.2", "fuels": "0.101.3", - "ioredis": "^5.4.1", - "mongodb": "^6.11.0", + "ioredis": "^5.7.0", + "mongodb": "^6.18.0", "node-cron": "3.0.3", "pg": "8.5.1", "redis": "4.7.0", "ts-node": "^10.9.2", - "typescript": "~5.4.5" + "typescript": "~5.4.5", + "uuid": "^13.0.0" }, "devDependencies": { "@commitlint/cli": "12.0.1", "@commitlint/config-conventional": "12.0.1", - "@trivago/prettier-plugin-sort-imports": "2.0.2", + "@trivago/prettier-plugin-sort-imports": "4.3.0", "@types/cors": "2.8.10", "@types/express": "4.17.11", "@types/morgan": "1.9.2", "@types/node": "20.6.0", - "@types/pg": "^8.11.6", + "@types/pg": "^8.15.5", + "@types/uuid": "^11.0.0", "@typescript-eslint/eslint-plugin": "6.5.0", "@typescript-eslint/parser": "6.5.0", "eslint": "7.22.0", @@ -45,7 +47,7 @@ "lint-staged": "10.5.4", "prettier": "2.2.1", "pretty-quick": "3.1.0", - "ts-node-dev": "1.1.6", + "ts-node-dev": "2.0.0", "tscpaths": "0.0.9" } } diff --git a/packages/worker/src/clients/mongoClient.ts b/packages/worker/src/clients/mongoClient.ts index 6da638925..5b692c288 100644 --- a/packages/worker/src/clients/mongoClient.ts +++ b/packages/worker/src/clients/mongoClient.ts @@ -21,6 +21,7 @@ export enum CollectionName { PREDICATE_BLOCKS = 'predicate_blocks', FUEL_ASSETS = 'fuel_assets', ASSET_BALANCE = 'asset_balance', + USER_BLOCK_CONTROL = 'user_block_control', } export interface SchemaAssetBalance extends Document { diff --git a/packages/worker/src/clients/psqlClient.ts b/packages/worker/src/clients/psqlClient.ts index 5d12c3f2c..73d6b0d16 100644 --- a/packages/worker/src/clients/psqlClient.ts +++ b/packages/worker/src/clients/psqlClient.ts @@ -56,7 +56,7 @@ export class PsqlClient { } } - async query (query: string, params?: string[]): Promise { + async query (query: string, params?: unknown[]): Promise { try { const { rows }: QueryResult = await this.client.query(query, params) if (rows.length === 1) return rows[0] diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 73afa9dac..f04e0d111 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -8,6 +8,7 @@ import AssetCron from "./queues/assetsValue/scheduler"; import assetQueue from "./queues/assetsValue/queue"; import { MongoDatabase } from "./clients/mongoClient"; import { PsqlClient } from "./clients"; +import { userBlockSyncQueue, userLogoutSyncQueue, UserBlockSyncCron } from "./queues/userBlockSync"; const { WORKER_PORT, @@ -51,7 +52,12 @@ const app = express(); const serverAdapter = new ExpressAdapter(); createBullBoard({ - queues: [new BullAdapter(balanceQueue), new BullAdapter(assetQueue)], + queues: [ + new BullAdapter(balanceQueue), + new BullAdapter(assetQueue), + new BullAdapter(userBlockSyncQueue), + new BullAdapter(userLogoutSyncQueue), + ], serverAdapter, }); @@ -66,6 +72,7 @@ PsqlClient.connect(); // schedulers BalanceCron.create(); AssetCron.create(); +UserBlockSyncCron.create(); app.listen(WORKER_PORT ?? 3063, () => console.log(`Server running on ${WORKER_PORT}`) diff --git a/packages/worker/src/queues/index.ts b/packages/worker/src/queues/index.ts index 531616190..4e12424c4 100644 --- a/packages/worker/src/queues/index.ts +++ b/packages/worker/src/queues/index.ts @@ -1,2 +1,3 @@ export * from './predicateBalance' -export * from './assetsValue' \ No newline at end of file +export * from './assetsValue' +export * from './userBlockSync' \ No newline at end of file diff --git a/packages/worker/src/queues/userBlockSync/constants.ts b/packages/worker/src/queues/userBlockSync/constants.ts new file mode 100644 index 000000000..c53a6ffb6 --- /dev/null +++ b/packages/worker/src/queues/userBlockSync/constants.ts @@ -0,0 +1,23 @@ +// Queue names +export const QUEUE_USER_BLOCK_SYNC = "QUEUE_USER_BLOCK_SYNC"; +export const QUEUE_USER_LOGOUT_SYNC = "QUEUE_USER_LOGOUT_SYNC"; + +// Scheduler configuration +export const CRON_EXPRESSION_USER_SYNC = "*/10 * * * * *"; // Every 10 seconds +export const INITIAL_DELAY_USER_SYNC = 3 * 1000; // 3 seconds initial delay + +// Block reading configuration +export const BLOCK_RANGE_SIZE = 1000; // Number of blocks to read per batch +export const MAX_BLOCKS_PER_USER = 5000; // Maximum blocks to process per user per cycle + +// Redis keys +export const REDIS_KEY_PREFIX = "user_block_sync"; +export const REDIS_KEY_LOGGED_USERS = `${REDIS_KEY_PREFIX}:logged_users`; +export const REDIS_KEY_USER_BLOCK = (userId: string) => `${REDIS_KEY_PREFIX}:user:${userId}:block`; +export const REDIS_KEY_USER_LAST_LOGIN = (userId: string) => `${REDIS_KEY_PREFIX}:user:${userId}:last_login`; + +// Hypersync endpoint +export const HYPERSYNC_ENDPOINT = "https://fuel.hypersync.xyz/query"; + +// Transaction status +export const TX_STATUS_SUCCESS = 1; diff --git a/packages/worker/src/queues/userBlockSync/index.ts b/packages/worker/src/queues/userBlockSync/index.ts new file mode 100644 index 000000000..d7e116d33 --- /dev/null +++ b/packages/worker/src/queues/userBlockSync/index.ts @@ -0,0 +1,6 @@ +export * from "./types"; +export * from "./constants"; +export * from "./utils"; +export { default as userBlockSyncQueue } from "./queue"; +export { default as userLogoutSyncQueue, triggerLogoutSync, getPersistedUserBlock } from "./logoutQueue"; +export { default as UserBlockSyncCron } from "./scheduler"; diff --git a/packages/worker/src/queues/userBlockSync/logoutQueue.ts b/packages/worker/src/queues/userBlockSync/logoutQueue.ts new file mode 100644 index 000000000..a53aa685e --- /dev/null +++ b/packages/worker/src/queues/userBlockSync/logoutQueue.ts @@ -0,0 +1,146 @@ +import Queue from "bull"; +import { redisConfig } from "@/clients"; +import { QUEUE_USER_LOGOUT_SYNC } from "./constants"; +import type { QueueUserLogoutSync } from "./types"; +import { + getUserLastBlock, + cleanupUserBlockSyncData, +} from "./utils"; +import { + CollectionName, + MongoDatabase, +} from "../../clients/mongoClient"; + +/** + * Schema for persisting user block state to MongoDB + */ +export interface SchemaUserBlockControl { + _id: string; // user_id + last_block: number; + last_sync: Date; + predicates: string[]; +} + +const userLogoutSyncQueue = new Queue(QUEUE_USER_LOGOUT_SYNC, { + redis: redisConfig, +}); + +/** + * User Logout Sync Queue + * + * This queue is triggered when a user logs out or their token expires. + * It persists the user's last synced block state from Redis to MongoDB + * for recovery when they log in again. + * + * Process: + * 1. Get user's last block state from Redis + * 2. Save to MongoDB (user_block_control collection) + * 3. Clean up Redis data for this user + */ +userLogoutSyncQueue.process(async (job) => { + const { user_id, last_block, predicates } = job.data; + + const db = await MongoDatabase.connect(); + const collection = db.getCollection( + CollectionName.USER_BLOCK_CONTROL + ); + + try { + // Get the actual last block from Redis (might be more recent than job data) + const redisLastBlock = await getUserLastBlock(user_id); + const blockToSave = Math.max(last_block, redisLastBlock); + + // Persist to MongoDB + await collection.updateOne( + { _id: user_id }, + { + $set: { + last_block: blockToSave, + last_sync: new Date(), + predicates: predicates, + }, + }, + { upsert: true } + ); + + // Clean up Redis data + await cleanupUserBlockSyncData(user_id); + + console.log( + `[${QUEUE_USER_LOGOUT_SYNC}] Persisted block ${blockToSave} for user ${user_id}` + ); + + return { + user_id, + persisted_block: blockToSave, + success: true, + }; + } catch (error) { + console.error( + `[${QUEUE_USER_LOGOUT_SYNC}] Error persisting state for user ${user_id}:`, + error + ); + throw error; + } +}); + +// Event handlers +userLogoutSyncQueue.on("completed", (job, result) => { + console.log( + `[${QUEUE_USER_LOGOUT_SYNC}] Job ${job.id} completed: user ${result.user_id} ` + + `block ${result.persisted_block}` + ); +}); + +userLogoutSyncQueue.on("failed", (job, err) => { + console.error( + `[${QUEUE_USER_LOGOUT_SYNC}] Job ${job.id} failed for user ${job.data.user_id}:`, + err.message + ); +}); + +/** + * Helper function to trigger logout sync from API + */ +export const triggerLogoutSync = async ( + userId: string, + predicates: string[] +): Promise => { + const lastBlock = await getUserLastBlock(userId); + + await userLogoutSyncQueue.add( + { + user_id: userId, + last_block: lastBlock, + predicates, + }, + { + attempts: 3, + backoff: { + type: "exponential", + delay: 1000, + }, + removeOnComplete: true, + removeOnFail: false, + // High priority for logout operations + priority: 1, + } + ); +}; + +/** + * Get user's last persisted block from MongoDB (for login recovery) + */ +export const getPersistedUserBlock = async ( + userId: string +): Promise => { + const db = await MongoDatabase.connect(); + const collection = db.getCollection( + CollectionName.USER_BLOCK_CONTROL + ); + + const record = await collection.findOne({ _id: userId }); + return record?.last_block ?? 0; +}; + +export default userLogoutSyncQueue; diff --git a/packages/worker/src/queues/userBlockSync/queue.ts b/packages/worker/src/queues/userBlockSync/queue.ts new file mode 100644 index 000000000..6a6fd7450 --- /dev/null +++ b/packages/worker/src/queues/userBlockSync/queue.ts @@ -0,0 +1,187 @@ +import Queue from "bull"; +import { redisConfig } from "@/clients"; +import { + QUEUE_USER_BLOCK_SYNC, + BLOCK_RANGE_SIZE, + MAX_BLOCKS_PER_USER, +} from "./constants"; +import type { QueueUserBlockSync, BlockSyncResult } from "./types"; +import { + getUserLastBlock, + setUserLastBlock, + getCurrentBlockHeight, + persistDepositsToDatabase, +} from "./utils"; +import { + CollectionName, + MongoDatabase, + type SchemaPredicateBalance, + type SchemaFuelAssets, + type SchemaPredicateBlocks, +} from "../../clients/mongoClient"; +import { + groupByTransaction, + makeDeposits, + syncBalance, + syncAssets, + syncPredicateBlock, +} from "../predicateBalance"; +import type { PredicateBalance } from "../predicateBalance/types"; +import { makeQuery } from "../predicateBalance/utils/envioQuery"; + +const userBlockSyncQueue = new Queue(QUEUE_USER_BLOCK_SYNC, { + redis: redisConfig, +}); + +/** + * User Block Sync Queue + * + * This queue processes block synchronization for logged-in users. + * + * Process: + * 1. Get user's last synced block from Redis + * 2. Fetch blocks in ranges (BLOCK_RANGE_SIZE) until reaching current tip or MAX_BLOCKS_PER_USER + * 3. Process transactions found for user's predicates + * 4. Update last synced block in Redis + * 5. If reached tip or processed max blocks, move to next user + */ +userBlockSyncQueue.process(async (job) => { + const { user_id, predicates } = job.data; + + if (!predicates || predicates.length === 0) { + return { + user_id, + blocks_processed: 0, + transactions_found: 0, + current_block: 0, + reached_tip: true, + } as BlockSyncResult; + } + + const db = await MongoDatabase.connect(); + const balanceCollection = db.getCollection( + CollectionName.PREDICATE_BALANCE + ); + const assetsCollection = db.getCollection(CollectionName.FUEL_ASSETS); + const priceCollection = db.getCollection(CollectionName.ASSET_BALANCE); + const predicateBlockCollection = db.getCollection( + CollectionName.PREDICATE_BLOCKS + ); + + let userLastBlock = await getUserLastBlock(user_id); + let currentTip: number; + + try { + currentTip = await getCurrentBlockHeight(); + } catch { + // If we can't get current tip, use a large number and let hypersync tell us + currentTip = userLastBlock + MAX_BLOCKS_PER_USER; + } + + let blocksProcessed = 0; + let totalTransactions = 0; + let reachedTip = false; + + try { + // Process each predicate for this user + for (const predicateAddress of predicates) { + let predicateLastBlock = userLastBlock; + + while (blocksProcessed < MAX_BLOCKS_PER_USER && !reachedTip) { + // Check if we've reached the tip + if (predicateLastBlock >= currentTip) { + reachedTip = true; + break; + } + + // Fetch transactions using existing query format + const response = await fetch( + "https://fuel.hypersync.xyz/query", + makeQuery({ + from_block: predicateLastBlock, + predicate_address: predicateAddress, + }) + ); + + const data = await response.json(); + const hypersyncData: PredicateBalance[] = data.data ?? []; + const nextBlock: number = data.next_block ?? predicateLastBlock + BLOCK_RANGE_SIZE; + + if (hypersyncData.length > 0) { + // Group and process transactions + const txGrouped = groupByTransaction(hypersyncData); + const txCount = Object.keys(txGrouped).length; + + if (txCount > 0) { + // Sync predicate block info + const predicateSyncBlock: SchemaPredicateBlocks = { + _id: predicateAddress, + blockNumber: nextBlock, + timestamp: Date.now(), + transactions: txCount, + }; + await syncPredicateBlock(predicateSyncBlock, predicateBlockCollection); + + // Process balance + const deposits = await makeDeposits(txGrouped, predicateAddress); + const assets = await syncAssets(deposits, assetsCollection); + await syncBalance(deposits, balanceCollection, assets, priceCollection); + + // Persist deposit transactions to PostgreSQL + const { created: depositsCreated } = await persistDepositsToDatabase( + deposits, + predicateAddress + ); + + totalTransactions += txCount + depositsCreated; + } + } + + // Update progress + predicateLastBlock = nextBlock; + blocksProcessed += BLOCK_RANGE_SIZE; + + // Check if we've reached the tip or no more data + if (predicateLastBlock >= currentTip || hypersyncData.length === 0) { + reachedTip = true; + } + } + } + + // Save final progress to Redis + const finalBlock = Math.max(userLastBlock, userLastBlock + blocksProcessed); + await setUserLastBlock(user_id, finalBlock); + + console.log( + `[${QUEUE_USER_BLOCK_SYNC}] User ${user_id}: processed ${blocksProcessed} blocks, ` + + `${totalTransactions} transactions, reached_tip: ${reachedTip}` + ); + + return { + user_id, + blocks_processed: blocksProcessed, + transactions_found: totalTransactions, + current_block: finalBlock, + reached_tip: reachedTip, + } as BlockSyncResult; + } catch (error) { + console.error(`[${QUEUE_USER_BLOCK_SYNC}] Error processing user ${user_id}:`, error); + throw error; + } +}); + +// Event handlers for monitoring +userBlockSyncQueue.on("completed", (job, result: BlockSyncResult) => { + console.log( + `[${QUEUE_USER_BLOCK_SYNC}] Job ${job.id} completed for user ${result.user_id}` + ); +}); + +userBlockSyncQueue.on("failed", (job, err) => { + console.error( + `[${QUEUE_USER_BLOCK_SYNC}] Job ${job.id} failed for user ${job.data.user_id}:`, + err.message + ); +}); + +export default userBlockSyncQueue; diff --git a/packages/worker/src/queues/userBlockSync/scheduler.ts b/packages/worker/src/queues/userBlockSync/scheduler.ts new file mode 100644 index 000000000..d96d6b564 --- /dev/null +++ b/packages/worker/src/queues/userBlockSync/scheduler.ts @@ -0,0 +1,104 @@ +import cron from "node-cron"; +import userBlockSyncQueue from "./queue"; +import { + CRON_EXPRESSION_USER_SYNC, + INITIAL_DELAY_USER_SYNC, + QUEUE_USER_BLOCK_SYNC, +} from "./constants"; +import { getLoggedUsersSortedByLogin } from "./utils"; + +/** + * Schedule jobs for all logged-in users + * Users are processed in order of most recent login (priority) + */ +const scheduleUserSync = async () => { + try { + const loggedUsers = await getLoggedUsersSortedByLogin(); + + if (loggedUsers.length === 0) { + return; + } + + console.log( + `[${QUEUE_USER_BLOCK_SYNC}] Scheduling sync for ${loggedUsers.length} logged users` + ); + + // Add jobs for each user with priority based on login order + for (let i = 0; i < loggedUsers.length; i++) { + const user = loggedUsers[i]; + + await userBlockSyncQueue.add( + { + user_id: user.user_id, + user_address: user.user_address, + predicates: user.predicates, + last_login: user.last_login, + }, + { + attempts: 3, + backoff: { + type: "exponential", + delay: 2000, + }, + removeOnComplete: true, + removeOnFail: false, + // Lower priority number = higher priority + // Most recent login gets priority 1, next gets 2, etc. + priority: i + 1, + // Prevent duplicate jobs for same user + jobId: `user_sync_${user.user_id}_${Date.now()}`, + } + ); + } + } catch (error) { + console.error(`[${QUEUE_USER_BLOCK_SYNC}] Error scheduling user sync:`, error); + } +}; + +class UserBlockSyncCron { + private static instance: UserBlockSyncCron; + private isRunning: boolean = false; + private cronJob: cron.ScheduledTask | null = null; + + private constructor() {} + + public static create(): UserBlockSyncCron { + if (!this.instance) { + this.instance = new UserBlockSyncCron(); + } + if (!this.instance.isRunning) { + this.instance.start(); + } + return this.instance; + } + + public start(): void { + if (this.isRunning) { + return; + } + + this.isRunning = true; + + console.log( + `[${QUEUE_USER_BLOCK_SYNC}] Starting scheduler with cron: ${CRON_EXPRESSION_USER_SYNC}` + ); + + // Initial delay before first run + setTimeout(() => { + scheduleUserSync(); + }, INITIAL_DELAY_USER_SYNC); + + // Schedule recurring job every 10 seconds + this.cronJob = cron.schedule(CRON_EXPRESSION_USER_SYNC, scheduleUserSync); + } + + public stop(): void { + if (this.cronJob) { + this.cronJob.stop(); + this.isRunning = false; + console.log(`[${QUEUE_USER_BLOCK_SYNC}] Scheduler stopped`); + } + } +} + +export default UserBlockSyncCron; diff --git a/packages/worker/src/queues/userBlockSync/types.ts b/packages/worker/src/queues/userBlockSync/types.ts new file mode 100644 index 000000000..d768e4916 --- /dev/null +++ b/packages/worker/src/queues/userBlockSync/types.ts @@ -0,0 +1,65 @@ +export interface QueueUserBlockSync { + user_id: string; + user_address: string; + predicates: string[]; // List of predicate addresses for this user + last_login: number; // Timestamp of last login for priority sorting +} + +export interface QueueUserLogoutSync { + user_id: string; + last_block: number; + predicates: string[]; +} + +export interface UserBlockState { + user_id: string; + last_block: number; + last_sync: number; // Timestamp + predicates_synced: string[]; +} + +export interface LoggedUser { + user_id: string; + user_address: string; + last_login: number; + predicates: string[]; +} + +export interface HypersyncResponse { + data: HypersyncTransaction[]; + next_block: number; +} + +export interface HypersyncTransaction { + tx_id: string; + block_height: number; + time: number; + status: number; + inputs: HypersyncInput[]; + outputs: HypersyncOutput[]; +} + +export interface HypersyncInput { + tx_id: string; + owner: string; + amount: string; + asset_id: string; + input_type: number; + recipient?: string; +} + +export interface HypersyncOutput { + tx_id: string; + to: string; + amount: string; + asset_id: string; + output_type: number; +} + +export interface BlockSyncResult { + user_id: string; + blocks_processed: number; + transactions_found: number; + current_block: number; + reached_tip: boolean; +} diff --git a/packages/worker/src/queues/userBlockSync/utils/index.ts b/packages/worker/src/queues/userBlockSync/utils/index.ts new file mode 100644 index 000000000..7058f3025 --- /dev/null +++ b/packages/worker/src/queues/userBlockSync/utils/index.ts @@ -0,0 +1,12 @@ +export * from "./redisHelpers"; +export * from "./persistDeposits"; + +// Re-export getCurrentBlockHeight for convenience +export const getCurrentBlockHeight = async (): Promise => { + const response = await fetch("https://fuel.hypersync.xyz/height"); + if (!response.ok) { + throw new Error(`Failed to get current block height: ${response.status}`); + } + const data = await response.json(); + return data.height ?? 0; +}; diff --git a/packages/worker/src/queues/userBlockSync/utils/persistDeposits.ts b/packages/worker/src/queues/userBlockSync/utils/persistDeposits.ts new file mode 100644 index 000000000..4b007ad5a --- /dev/null +++ b/packages/worker/src/queues/userBlockSync/utils/persistDeposits.ts @@ -0,0 +1,241 @@ +import { PsqlClient } from "@/clients"; +import type { SchemaPredicateBalance } from "../../../clients/mongoClient"; +import { v4 as uuidv4 } from "uuid"; + +/** + * Deposit transaction type enum matching the API + */ +export const TRANSACTION_TYPE_DEPOSIT = "DEPOSIT"; +export const TRANSACTION_STATUS_SUCCESS = "success"; + +/** + * Structure for a deposit transaction to be inserted in PostgreSQL + */ +export interface DepositTransaction { + id: string; + name: string; + hash: string; + type: string; + tx_data: object; + status: string; + summary: object | null; + send_time: Date; + gas_used: string | null; + resume: object; + network: object; + predicate_id: string; + created_by: string | null; + created_at: Date; + updated_at: Date; +} + +/** + * Check if a deposit transaction already exists in the database + */ +export const depositExists = async ( + db: PsqlClient, + hash: string, + predicateId: string +): Promise => { + const result = await db.query( + `SELECT id FROM transactions + WHERE hash = $1 AND predicate_id = $2 AND type = $3 + LIMIT 1`, + [hash, predicateId, TRANSACTION_TYPE_DEPOSIT] + ); + + return !!result; +}; + +/** + * Get predicate info by address + */ +export const getPredicateByAddress = async ( + db: PsqlClient, + predicateAddress: string +): Promise<{ id: string; owner_id: string; configurable: string } | null> => { + const result = await db.query( + `SELECT id, owner_id, configurable FROM predicates + WHERE predicate_address = $1 + LIMIT 1`, + [predicateAddress] + ); + + return result || null; +}; + +/** + * Get network info for a predicate + */ +export const getNetworkForPredicate = async ( + db: PsqlClient, + predicateId: string +): Promise<{ url: string; chain_id: number } | null> => { + const result = await db.query( + `SELECT n.url, n.chain_id + FROM predicates p + JOIN networks n ON p.network_id = n.id + WHERE p.id = $1 + LIMIT 1`, + [predicateId] + ); + + return result || null; +}; + +/** + * Create a deposit transaction in the database + */ +export const createDepositTransaction = async ( + db: PsqlClient, + deposit: SchemaPredicateBalance, + predicateId: string, + ownerId: string | null, + network: { url: string; chainId: number } +): Promise => { + const txId = uuidv4(); + const now = new Date(); + + // Build resume object similar to API format + const resume = { + hash: deposit.tx_id, + status: TRANSACTION_STATUS_SUCCESS, + witnesses: [], + predicate: { + id: predicateId, + address: deposit.predicate, + }, + id: deposit.tx_id, + }; + + // Build minimal tx_data + const txData = { + type: 0, // Script transaction + outputs: [], + inputs: [], + }; + + // Build summary with deposit info + const summary = { + operations: [ + { + name: "Deposit", + from: { type: "Unknown" }, + to: { + type: "Predicate", + address: deposit.predicate, + }, + assetsSent: [ + { + assetId: deposit.assetId, + amount: deposit.amount.toString(), + }, + ], + }, + ], + }; + + try { + await db.query( + `INSERT INTO transactions ( + id, name, hash, type, tx_data, status, summary, + send_time, gas_used, resume, network, + predicate_id, created_by, created_at, updated_at + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, + $8, $9, $10, $11, + $12, $13, $14, $15 + ) + ON CONFLICT (hash, predicate_id) DO NOTHING`, + [ + txId, + "Deposit", + deposit.tx_id.startsWith("0x") ? deposit.tx_id.slice(2) : deposit.tx_id, + TRANSACTION_TYPE_DEPOSIT, + JSON.stringify(txData), + TRANSACTION_STATUS_SUCCESS, + JSON.stringify(summary), + deposit.createdAt, + null, + JSON.stringify(resume), + JSON.stringify(network), + predicateId, + ownerId, + now, + now, + ] + ); + + return txId; + } catch (error) { + console.error("[PERSIST_DEPOSIT] Error creating deposit transaction:", error); + return null; + } +}; + +/** + * Persist multiple deposits to PostgreSQL + * Only persists deposits where isDeposit = true + */ +export const persistDepositsToDatabase = async ( + deposits: SchemaPredicateBalance[], + predicateAddress: string +): Promise<{ created: number; skipped: number }> => { + const db = await PsqlClient.connect(); + let created = 0; + let skipped = 0; + + // Get predicate info + const predicate = await getPredicateByAddress(db, predicateAddress); + if (!predicate) { + console.warn( + `[PERSIST_DEPOSIT] Predicate not found for address: ${predicateAddress}` + ); + return { created: 0, skipped: deposits.length }; + } + + // Get network info + const networkInfo = await getNetworkForPredicate(db, predicate.id); + const network = networkInfo + ? { url: networkInfo.url, chainId: networkInfo.chain_id } + : { url: "https://mainnet.fuel.network/v1/graphql", chainId: 9889 }; + + // Filter only actual deposits (not withdrawals) + const actualDeposits = deposits.filter((d) => d.isDeposit && d.amount > 0); + + for (const deposit of actualDeposits) { + // Check if deposit already exists + const hash = deposit.tx_id.startsWith("0x") + ? deposit.tx_id.slice(2) + : deposit.tx_id; + + const exists = await depositExists(db, hash, predicate.id); + if (exists) { + skipped++; + continue; + } + + // Create the deposit transaction + const txId = await createDepositTransaction( + db, + deposit, + predicate.id, + predicate.owner_id, + network + ); + + if (txId) { + created++; + } else { + skipped++; + } + } + + if (created > 0) { + console.log( + `[PERSIST_DEPOSIT] Created ${created} deposit transactions for ${predicateAddress}` + ); + } + + return { created, skipped }; +}; diff --git a/packages/worker/src/queues/userBlockSync/utils/redisHelpers.ts b/packages/worker/src/queues/userBlockSync/utils/redisHelpers.ts new file mode 100644 index 000000000..8cb544fe2 --- /dev/null +++ b/packages/worker/src/queues/userBlockSync/utils/redisHelpers.ts @@ -0,0 +1,139 @@ +import redisClient from "@/clients/redisClient"; +import { + REDIS_KEY_LOGGED_USERS, + REDIS_KEY_USER_BLOCK, + REDIS_KEY_USER_LAST_LOGIN, +} from "../constants"; +import type { LoggedUser, UserBlockState } from "../types"; + +/** + * Register a user as logged in with their predicates + */ +export const registerLoggedUser = async ( + userId: string, + userAddress: string, + predicates: string[], + lastLogin: number = Date.now() +): Promise => { + const userData: LoggedUser = { + user_id: userId, + user_address: userAddress, + last_login: lastLogin, + predicates, + }; + + await redisClient.hset(REDIS_KEY_LOGGED_USERS, userId, JSON.stringify(userData)); + await redisClient.set(REDIS_KEY_USER_LAST_LOGIN(userId), lastLogin.toString()); +}; + +/** + * Remove a user from logged users set + */ +export const unregisterLoggedUser = async (userId: string): Promise => { + await redisClient.hdel(REDIS_KEY_LOGGED_USERS, userId); + await redisClient.del(REDIS_KEY_USER_LAST_LOGIN(userId)); +}; + +/** + * Get all logged users sorted by most recent login + */ +export const getLoggedUsersSortedByLogin = async (): Promise => { + const usersData = await redisClient.hgetall(REDIS_KEY_LOGGED_USERS); + + if (!usersData || Object.keys(usersData).length === 0) { + return []; + } + + const users: LoggedUser[] = Object.values(usersData).map((data) => + JSON.parse(data) + ); + + // Sort by last_login descending (most recent first) + return users.sort((a, b) => b.last_login - a.last_login); +}; + +/** + * Get the last synced block for a user + */ +export const getUserLastBlock = async (userId: string): Promise => { + const block = await redisClient.get(REDIS_KEY_USER_BLOCK(userId)); + return block ? parseInt(block, 10) : 0; +}; + +/** + * Update the last synced block for a user + */ +export const setUserLastBlock = async ( + userId: string, + blockNumber: number +): Promise => { + await redisClient.set(REDIS_KEY_USER_BLOCK(userId), blockNumber.toString()); +}; + +/** + * Get full user block state + */ +export const getUserBlockState = async ( + userId: string +): Promise => { + const [block, userData] = await Promise.all([ + redisClient.get(REDIS_KEY_USER_BLOCK(userId)), + redisClient.hget(REDIS_KEY_LOGGED_USERS, userId), + ]); + + if (!userData) { + return null; + } + + const user: LoggedUser = JSON.parse(userData); + + return { + user_id: userId, + last_block: block ? parseInt(block, 10) : 0, + last_sync: Date.now(), + predicates_synced: user.predicates, + }; +}; + +/** + * Check if a user is currently logged in + */ +export const isUserLoggedIn = async (userId: string): Promise => { + const exists = await redisClient.hexists(REDIS_KEY_LOGGED_USERS, userId); + return exists === 1; +}; + +/** + * Update user's last login time (for priority refresh) + */ +export const updateUserLastLogin = async ( + userId: string, + timestamp: number = Date.now() +): Promise => { + const userData = await redisClient.hget(REDIS_KEY_LOGGED_USERS, userId); + + if (userData) { + const user: LoggedUser = JSON.parse(userData); + user.last_login = timestamp; + await redisClient.hset(REDIS_KEY_LOGGED_USERS, userId, JSON.stringify(user)); + await redisClient.set(REDIS_KEY_USER_LAST_LOGIN(userId), timestamp.toString()); + } +}; + +/** + * Get count of logged users + */ +export const getLoggedUsersCount = async (): Promise => { + return await redisClient.hlen(REDIS_KEY_LOGGED_USERS); +}; + +/** + * Clean up all user block sync data (for maintenance) + */ +export const cleanupUserBlockSyncData = async (userId: string): Promise => { + await Promise.all([ + redisClient.hdel(REDIS_KEY_LOGGED_USERS, userId), + redisClient.del(REDIS_KEY_USER_BLOCK(userId)), + redisClient.del(REDIS_KEY_USER_LAST_LOGIN(userId)), + ]); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7303d0617..086e68981 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,12 +4,25 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + glob@>=10.2.0 <10.5.0: 10.5.0 + braces@<3.0.3: 3.0.3 + semver@>=7.0.0 <7.5.2: 7.5.2 + qs@<6.14.1: 6.14.1 + trim-newlines@<3.0.1: 3.0.1 + js-yaml@<3.13.1: 3.14.1 + uglify-js@<2.6.0: 3.19.3 + minimatch@<3.1.4: 3.1.4 + minimatch@>=5.0.0 <5.1.8: 5.1.8 + minimatch@>=9.0.0 <9.0.7: 9.0.7 + minimatch@>=10.0.0 <10.2.3: 10.2.3 + importers: .: devDependencies: turbo: - specifier: ^1.13.3 + specifier: ^1.13.4 version: 1.13.4 packages/api: @@ -24,8 +37,8 @@ importers: specifier: 1.9.0 version: 1.9.0 '@opentelemetry/exporter-trace-otlp-proto': - specifier: 0.201.1 - version: 0.201.1(@opentelemetry/api@1.9.0) + specifier: 0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': specifier: 0.201.1 version: 0.201.1(@opentelemetry/api@1.9.0) @@ -63,14 +76,14 @@ importers: specifier: 11.0.0 version: 11.0.0 axios: - specifier: 1.5.1 - version: 1.5.1 + specifier: 1.13.5 + version: 1.13.5 bakosafe: - specifier: 0.2.1-beta.1 - version: 0.2.1-beta.1(fuels@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))) + specifier: 0.6.0 + version: 0.6.0(fuels@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.4.5) body-parser: - specifier: 1.20.2 - version: 1.20.2 + specifier: 1.20.4 + version: 1.20.4 cheerio: specifier: 1.0.0-rc.12 version: 1.0.0-rc.12 @@ -93,17 +106,17 @@ importers: specifier: 16.4.5 version: 16.4.5 express: - specifier: 4.17.1 - version: 4.17.1 + specifier: 4.21.2 + version: 4.21.2 express-joi-validation: specifier: 5.0.0 version: 5.0.0(joi@17.4.0) fuels: specifier: 0.101.3 - version: 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + version: 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) glob: - specifier: 10.3.15 - version: 10.3.15 + specifier: 10.5.0 + version: 10.5.0 handlebars: specifier: 4.7.8 version: 4.7.8 @@ -111,8 +124,8 @@ importers: specifier: 17.4.0 version: 17.4.0 jsonwebtoken: - specifier: 9.0.1 - version: 9.0.1 + specifier: 9.0.3 + version: 9.0.3 morgan: specifier: 1.10.0 version: 1.10.0 @@ -120,17 +133,20 @@ importers: specifier: 3.0.3 version: 3.0.3 nodemailer: - specifier: 6.9.8 - version: 6.9.8 + specifier: 8.0.1 + version: 8.0.1 patch-package: specifier: 8.0.0 version: 8.0.0 pg: specifier: 8.5.1 version: 8.5.1 + pino: + specifier: 9.6.0 + version: 9.6.0 qs: - specifier: 6.12.1 - version: 6.12.1 + specifier: 6.14.1 + version: 6.14.1 redis: specifier: 4.7.0 version: 4.7.0 @@ -143,6 +159,9 @@ importers: socket.io-client: specifier: 4.7.5 version: 4.7.5 + svix: + specifier: 1.76.1 + version: 1.76.1 ts-node: specifier: 10.9.2 version: 10.9.2(@types/node@20.6.0)(typescript@5.4.5) @@ -150,8 +169,8 @@ importers: specifier: 3.15.0 version: 3.15.0 typeorm: - specifier: 0.3.20 - version: 0.3.20(ioredis@5.7.0)(pg@8.5.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.6.0)(typescript@5.4.5)) + specifier: 0.3.28 + version: 0.3.28(ioredis@5.9.2)(mongodb@6.21.0)(pg@8.5.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.6.0)(typescript@5.4.5)) typescript: specifier: ~5.4.5 version: 5.4.5 @@ -163,8 +182,8 @@ importers: specifier: 12.0.1 version: 12.0.1 '@trivago/prettier-plugin-sort-imports': - specifier: 2.0.2 - version: 2.0.2(prettier@2.2.1) + specifier: 4.3.0 + version: 4.3.0(prettier@2.2.1) '@types/cors': specifier: 2.8.10 version: 2.8.10 @@ -175,8 +194,8 @@ importers: specifier: 8.1.0 version: 8.1.0 '@types/jsonwebtoken': - specifier: 9.0.2 - version: 9.0.2 + specifier: 9.0.10 + version: 9.0.10 '@types/morgan': specifier: 1.9.2 version: 1.9.2 @@ -186,6 +205,9 @@ importers: '@types/node-cron': specifier: 3.0.11 version: 3.0.11 + '@types/qs': + specifier: 6.14.0 + version: 6.14.0 '@types/supertest': specifier: 2.0.10 version: 2.0.10 @@ -216,6 +238,9 @@ importers: lint-staged: specifier: 10.5.4 version: 10.5.4 + pino-pretty: + specifier: 11.2.2 + version: 11.2.2 prettier: specifier: 2.2.1 version: 2.2.1 @@ -226,14 +251,17 @@ importers: specifier: 6.1.3 version: 6.1.3 ts-node-dev: - specifier: 1.1.6 - version: 1.1.6(typescript@5.4.5) + specifier: 2.0.0 + version: 2.0.0(@types/node@20.6.0)(typescript@5.4.5) + tsc-alias: + specifier: 1.8.16 + version: 1.8.16 tscpaths: specifier: 0.0.9 version: 0.0.9 tsx: - specifier: 4.19.3 - version: 4.19.3 + specifier: 4.21.0 + version: 4.21.0 wait-on: specifier: 8.0.3 version: 8.0.3 @@ -265,28 +293,34 @@ importers: dependencies: '@socket.io/redis-adapter': specifier: ^8.3.0 - version: 8.3.0(socket.io-adapter@2.5.5) + version: 8.3.0(socket.io-adapter@2.5.6) axios: - specifier: 1.5.1 - version: 1.5.1 + specifier: 1.13.5 + version: 1.13.5 bakosafe: - specifier: 0.2.1-beta.1 - version: 0.2.1-beta.1(fuels@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))) + specifier: 0.6.0 + version: 0.6.0(fuels@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.4.5) + date-fns: + specifier: 2.30.0 + version: 2.30.0 express: - specifier: 4.17.1 - version: 4.17.1 + specifier: 4.21.2 + version: 4.21.2 express-joi-validation: specifier: 5.0.0 version: 5.0.0(joi@17.13.3) fuels: specifier: 0.101.3 - version: 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + version: 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) ioredis: specifier: ^5.7.0 - version: 5.7.0 + version: 5.9.2 pg: specifier: 8.5.1 version: 8.5.1 + pino: + specifier: 9.6.0 + version: 9.6.0 socket.io: specifier: 4.7.2 version: 4.7.2 @@ -307,8 +341,8 @@ importers: specifier: 12.0.1 version: 12.0.1 '@trivago/prettier-plugin-sort-imports': - specifier: 2.0.2 - version: 2.0.2(prettier@2.2.1) + specifier: 4.3.0 + version: 4.3.0(prettier@2.2.1) '@types/cors': specifier: 2.8.10 version: 2.8.10 @@ -319,8 +353,8 @@ importers: specifier: ^29.5.14 version: 29.5.14 '@types/jsonwebtoken': - specifier: 9.0.2 - version: 9.0.2 + specifier: 9.0.10 + version: 9.0.10 '@types/morgan': specifier: 1.9.2 version: 1.9.2 @@ -329,7 +363,7 @@ importers: version: 20.6.0 '@types/pg': specifier: ^8.15.5 - version: 8.15.5 + version: 8.16.0 '@types/supertest': specifier: 2.0.10 version: 2.0.10 @@ -357,6 +391,9 @@ importers: lint-staged: specifier: 10.5.4 version: 10.5.4 + pino-pretty: + specifier: 11.2.2 + version: 11.2.2 prettier: specifier: 2.2.1 version: 2.2.1 @@ -368,10 +405,10 @@ importers: version: 6.1.3 ts-jest: specifier: ^29.4.1 - version: 29.4.1(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.3)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.6.0)(ts-node@10.9.2(@types/node@20.6.0)(typescript@5.4.5)))(typescript@5.4.5) + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.6.0)(ts-node@10.9.2(@types/node@20.6.0)(typescript@5.4.5)))(typescript@5.4.5) ts-node-dev: - specifier: 1.1.6 - version: 1.1.6(typescript@5.4.5) + specifier: 2.0.0 + version: 2.0.0(@types/node@20.6.0)(typescript@5.4.5) tscpaths: specifier: 0.0.9 version: 0.0.9 @@ -379,11 +416,11 @@ importers: packages/worker: dependencies: '@bull-board/api': - specifier: ^6.5.3 - version: 6.12.0(@bull-board/ui@6.12.0) + specifier: ^6.12.0 + version: 6.16.4(@bull-board/ui@6.16.4) '@bull-board/express': - specifier: ^6.5.3 - version: 6.12.0 + specifier: ^6.12.0 + version: 6.16.4 '@envio-dev/hypersync-client': specifier: 0.6.2 version: 0.6.2 @@ -394,20 +431,20 @@ importers: specifier: 3.0.11 version: 3.0.11 bull: - specifier: ^4.16.4 + specifier: ^4.16.5 version: 4.16.5 express: - specifier: 4.17.1 - version: 4.17.1 + specifier: 4.21.2 + version: 4.21.2 fuels: specifier: 0.101.3 - version: 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + version: 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) ioredis: - specifier: ^5.4.1 - version: 5.7.0 + specifier: ^5.7.0 + version: 5.9.2 mongodb: - specifier: ^6.11.0 - version: 6.18.0 + specifier: ^6.18.0 + version: 6.21.0 node-cron: specifier: 3.0.3 version: 3.0.3 @@ -423,6 +460,9 @@ importers: typescript: specifier: ~5.4.5 version: 5.4.5 + uuid: + specifier: ^13.0.0 + version: 13.0.0 devDependencies: '@commitlint/cli': specifier: 12.0.1 @@ -431,8 +471,8 @@ importers: specifier: 12.0.1 version: 12.0.1 '@trivago/prettier-plugin-sort-imports': - specifier: 2.0.2 - version: 2.0.2(prettier@2.2.1) + specifier: 4.3.0 + version: 4.3.0(prettier@2.2.1) '@types/cors': specifier: 2.8.10 version: 2.8.10 @@ -446,8 +486,11 @@ importers: specifier: 20.6.0 version: 20.6.0 '@types/pg': - specifier: ^8.11.6 - version: 8.15.5 + specifier: ^8.15.5 + version: 8.16.0 + '@types/uuid': + specifier: ^11.0.0 + version: 11.0.0 '@typescript-eslint/eslint-plugin': specifier: 6.5.0 version: 6.5.0(@typescript-eslint/parser@6.5.0(eslint@7.22.0)(typescript@5.4.5))(eslint@7.22.0)(typescript@5.4.5) @@ -476,46 +519,46 @@ importers: specifier: 3.1.0 version: 3.1.0(prettier@2.2.1) ts-node-dev: - specifier: 1.1.6 - version: 1.1.6(typescript@5.4.5) + specifier: 2.0.0 + version: 2.0.0(@types/node@20.6.0)(typescript@5.4.5) tscpaths: specifier: 0.0.9 version: 0.0.9 packages: - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} '@babel/code-frame@7.12.11': resolution: {integrity: sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==} - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.0': - resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} - '@babel/core@7.13.10': - resolution: {integrity: sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.0': - resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + '@babel/generator@7.17.7': + resolution: {integrity: sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==} engines: {node: '>=6.9.0'} - '@babel/generator@7.13.9': - resolution: {integrity: sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} - '@babel/generator@7.28.0': - resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.27.2': - resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + '@babel/helper-environment-visitor@7.24.7': + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} engines: {node: '>=6.9.0'} '@babel/helper-function-name@7.24.7': @@ -526,18 +569,22 @@ packages: resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.27.1': - resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + '@babel/helper-hoist-variables@7.24.7': + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.27.3': - resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.27.1': - resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} '@babel/helper-split-export-declaration@7.24.7': @@ -548,29 +595,24 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.2': - resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==} + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} engines: {node: '>=6.9.0'} '@babel/highlight@7.25.9': resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.13.10': - resolution: {integrity: sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.28.0': - resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} engines: {node: '>=6.0.0'} hasBin: true @@ -595,8 +637,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-attributes@7.27.1': - resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -611,8 +653,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.27.1': - resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -659,32 +701,34 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.28.2': - resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} - '@babel/template@7.27.2': - resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.13.0': - resolution: {integrity: sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ==} + '@babel/traverse@7.23.2': + resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} + engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.0': - resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} - '@babel/types@7.13.0': - resolution: {integrity: sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==} + '@babel/types@7.17.0': + resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} + engines: {node: '>=6.9.0'} - '@babel/types@7.28.2': - resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} '@balena/dockerignore@1.0.2': @@ -693,16 +737,20 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@bull-board/api@6.12.0': - resolution: {integrity: sha512-tlMQTc0EAiYbv8gjR0lCYAXDL1Uc8/dPD9tKjxvG+m9lUUpPAoRUXUHNtzwVQo+AHiF6WHEK9iA3+8KHJamc/w==} + '@bull-board/api@6.16.4': + resolution: {integrity: sha512-fn4O+QbA3mRj0rEE41mvwbvtiiv0UYgnxQ9ErWb9n74EwIC/yZbiyxQ+Gh/ehU9u7B0PuaNyR0IOG/h3DGo1Mg==} peerDependencies: - '@bull-board/ui': 6.12.0 + '@bull-board/ui': 6.16.4 + + '@bull-board/express@6.16.4': + resolution: {integrity: sha512-znKZGrqBtHh3iU73TvJherEY1OforQ10hcLMGer1ktRTD+5BxyLedIlhdZIxJsn+ComQQcmEySbqJSF2b78UOA==} - '@bull-board/express@6.12.0': - resolution: {integrity: sha512-wvydsoc/nX7OWWZIQ0TbjLST47nfKHMsWKZj26e+6R9YbvUtxZuKXpqWdDrRY/slpXUzvN0+3GkttMOUkuT92Q==} + '@bull-board/ui@6.16.4': + resolution: {integrity: sha512-5Yv+4g0rDvBBq2RxaUewSEwD8ywvqCX6lKlzPM5Aaf0+4cxGoENQRZNcBaAIKX4+fAzAbdVB4VGP4NUgtx5LVg==} - '@bull-board/ui@6.12.0': - resolution: {integrity: sha512-a1+9bUlNViXIQcO9KPOd1EP66Ts4caYHgsc5OuunDMZ9Q6Y7sfrQXMhfnv3JWahSNo3hiX6Fc8wIL4p/bcTwsw==} + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} '@commitlint/cli@12.0.1': resolution: {integrity: sha512-V+cMYNHJOr40XT9Kvz3Vrz1Eh7QE1rjQrUbifawDAqcOrBJFuoXwU2SAcRtYFCSqFy9EhbreQGhZFs8dYb90KA==} @@ -773,6 +821,9 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + '@envio-dev/hypersync-client-darwin-arm64@0.6.2': resolution: {integrity: sha512-dDIuQqEgARR1JYodbGkmck1i9qbYEidc4Kw4DOrRKQ0uZFwflI4o8wm3P+G/ofc1iXwp4pm7jqNUGzZDpK9pqA==} engines: {node: '>= 10'} @@ -813,320 +864,476 @@ packages: resolution: {integrity: sha512-wgp0UmblW8yn/q5NMkVYPFDvOmgHWPTibl/QJYyYK2KXqAMHssUjP07ayduiZCexQOZ94Agpv4SvmYxQNjGBIA==} engines: {node: '>= 10'} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.3': resolution: {integrity: sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.8': - resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.3': resolution: {integrity: sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.8': - resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.3': resolution: {integrity: sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.8': - resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.3': resolution: {integrity: sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.8': - resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.3': resolution: {integrity: sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.8': - resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.3': resolution: {integrity: sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.8': - resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.3': resolution: {integrity: sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.8': - resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.3': resolution: {integrity: sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.8': - resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.3': resolution: {integrity: sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.8': - resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.3': resolution: {integrity: sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.8': - resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.3': resolution: {integrity: sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.8': - resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.3': resolution: {integrity: sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.8': - resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.3': resolution: {integrity: sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.8': - resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.3': resolution: {integrity: sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.8': - resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.3': resolution: {integrity: sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.8': - resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.3': resolution: {integrity: sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.8': - resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.3': resolution: {integrity: sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.8': - resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.25.3': resolution: {integrity: sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.25.8': - resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.3': resolution: {integrity: sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.8': - resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.25.3': resolution: {integrity: sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.25.8': - resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.3': resolution: {integrity: sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.8': - resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.8': - resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.3': resolution: {integrity: sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.8': - resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.3': resolution: {integrity: sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.8': - resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.3': resolution: {integrity: sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.8': - resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.3': resolution: {integrity: sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.8': - resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/eslintrc@0.4.3': @@ -1224,8 +1431,8 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@grpc/grpc-js@1.13.4': - resolution: {integrity: sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==} + '@grpc/grpc-js@1.14.3': + resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} engines: {node: '>=12.10.0'} '@grpc/proto-loader@0.7.15': @@ -1233,22 +1440,19 @@ packages: engines: {node: '>=6'} hasBin: true + '@grpc/proto-loader@0.8.0': + resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==} + engines: {node: '>=6'} + hasBin: true + '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} '@hapi/topo@5.1.0': resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - '@ioredis/commands@1.3.0': - resolution: {integrity: sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==} - - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} + '@ioredis/commands@1.5.0': + resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -1331,6 +1535,9 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1338,8 +1545,8 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.30': - resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -1347,8 +1554,8 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@mongodb-js/saslprep@1.3.0': - resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==} + '@mongodb-js/saslprep@1.4.5': + resolution: {integrity: sha512-k64Lbyb7ycCSXHSLzxVdb2xsKGPMvYZfCICXvDsI8Z65CeWQzTEKS4YmGbnqw+U9RBvLPTsB6UCmwkgsDTGWIw==} '@mrmlnc/readdir-enhanced@2.2.1': resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} @@ -1384,6 +1591,10 @@ packages: cpu: [x64] os: [win32] + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.3.0': resolution: {integrity: sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==} @@ -1394,6 +1605,14 @@ packages: resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.3.3': resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==} engines: {node: '>= 16'} @@ -1406,6 +1625,13 @@ packages: resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@noble/secp256k1@2.3.0': + resolution: {integrity: sha512-0TQed2gcBbIrh7Ccyw+y/uZQvbJwm7Ao4scBUxqpBCcsOlZG0O4KGfjtNAy/li4W8n1xt3dxrwJ0beZ2h2G6Kw==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1426,6 +1652,10 @@ packages: resolution: {integrity: sha512-IxcFDP1IGMDemVFG2by/AMK+/o6EuBQ8idUq3xZ6MxgQGeumYZuX5OwR0h9HuvcUc/JPjQGfU5OHKIKYDJcXeA==} engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.212.0': + resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.52.1': resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==} engines: {node: '>=14'} @@ -1468,6 +1698,18 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.5.0': + resolution: {integrity: sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.5.1': + resolution: {integrity: sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/exporter-logs-otlp-grpc@0.201.1': resolution: {integrity: sha512-ACV2Az9BHRcAaPMYBnYMwKHNn2JwkzzsT3cdeG6+Tokm47fFfpf2xk3sq3QvX0Gk+TXW7q6d+OfBuYfWoAud2g==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1528,6 +1770,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-proto@0.212.0': + resolution: {integrity: sha512-d1ivqPT0V+i0IVOOdzGaLqonjtlk5jYrW7ItutWzXL/Mk+PiYb59dymy/i2reot9dDnBFWfrsvxyqdutGF5Vig==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-zipkin@2.0.1': resolution: {integrity: sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1702,6 +1950,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.212.0': + resolution: {integrity: sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-grpc-exporter-base@0.201.1': resolution: {integrity: sha512-Y0h9hiMvNtUuXUMkYNAt81hxnFuOHHSeu/RC+pXcHe7S6ac0ROlcjdabBKmYSadJxRrP4YfLahLRuNkVtZow4w==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1714,6 +1968,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-transformer@0.212.0': + resolution: {integrity: sha512-bj7zYFOg6Db7NUwsRZQ/WoVXpAf41WY2gsd3kShSfdpZQDRKHWJiRZIg7A8HvWsf97wb05rMFzPbmSHyjEl9tw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/propagator-b3@2.0.1': resolution: {integrity: sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1742,12 +2002,24 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.5.1': + resolution: {integrity: sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-logs@0.201.1': resolution: {integrity: sha512-Ug8gtpssUNUnfpotB9ZhnSsPSGDu+7LngTMgKl31mmVJwLAKyl6jC8diZrMcGkSgBh0o5dbg9puvLyR25buZfw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.212.0': + resolution: {integrity: sha512-qglb5cqTf0mOC1sDdZ7nfrPjgmAqs2OxkzOPIf2+Rqx8yKBK0pS7wRtB1xH30rqahBIut9QJDbDePyvtyqvH/Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-metrics@1.30.1': resolution: {integrity: sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==} engines: {node: '>=14'} @@ -1760,6 +2032,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.5.1': + resolution: {integrity: sha512-RKMn3QKi8nE71ULUo0g/MBvq1N4icEBo7cQSKnL3URZT16/YH3nSVgWegOjwx7FRBTrjOIkMJkCUn/ZFIEfn4A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + '@opentelemetry/sdk-node@0.201.1': resolution: {integrity: sha512-OdkYe6ZEFbPq+YXhebuiYpPECIBrrKgFJoAQVATllKlB5RDQDTE4J84/8LwGfQqSxBiSK2u1aSaFpzgBVoBrKA==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1778,6 +2056,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.5.1': + resolution: {integrity: sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-node@2.0.1': resolution: {integrity: sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==} engines: {node: ^18.19.0 || >=20.6.0} @@ -1871,115 +2155,149 @@ packages: peerDependencies: '@redis/client': ^1.0.0 - '@rollup/rollup-android-arm-eabi@4.46.2': - resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.46.2': - resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.46.2': - resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.46.2': - resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.46.2': - resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.46.2': - resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': - resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.46.2': - resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.46.2': - resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.46.2': - resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': - resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.46.2': - resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.46.2': - resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.46.2': - resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.46.2': - resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.46.2': - resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.46.2': - resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.46.2': - resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.46.2': - resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.46.2': - resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} cpu: [x64] os: [win32] '@scure/base@1.1.9': resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + '@scure/bip32@1.4.0': resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + '@scure/bip39@1.3.0': resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@sentry/core@8.32.0': resolution: {integrity: sha512-+xidTr0lZ0c755tq4k75dXPEb8PA+qvIefW3U9+dQMORLokBrYoKYMf5zZTG2k/OfSJS6OSxatUj36NFuCs3aA==} engines: {node: '>=14.18'} @@ -2020,8 +2338,8 @@ packages: '@sideway/pinpoint@2.0.0': resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -2029,6 +2347,9 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -2041,16 +2362,23 @@ packages: '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@testcontainers/postgresql@11.0.0': resolution: {integrity: sha512-cw2vIz5MS1AjXkxcPmOQh0NRdh7p6frSLOkjsBFuR5lUo3qnV3LmvCfanyRzgXs5aHGJLwc0mlQNfXnKedBF6Q==} - '@trivago/prettier-plugin-sort-imports@2.0.2': - resolution: {integrity: sha512-esk6vplzXYwXQs079wBbKog4AFuZfxpJU+MygiijV0wbAibI0tEm+diFFhYP7B2lAaKKdU4+w+BW+McNZCw9HA==} + '@trivago/prettier-plugin-sort-imports@4.3.0': + resolution: {integrity: sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==} peerDependencies: - prettier: ^2.2.1 + '@vue/compiler-sfc': 3.x + prettier: 2.x - 3.x + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} '@tsconfig/node12@1.0.11': resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} @@ -2104,14 +2432,14 @@ packages: '@types/docker-modem@3.0.6': resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} - '@types/dockerode@3.3.42': - resolution: {integrity: sha512-U1jqHMShibMEWHdxYhj3rCMNCiLx5f35i4e3CEUuW+JSSszc/tVqc6WCAPdhwBymG5R/vgbcceagK0St7Cq6Eg==} + '@types/dockerode@3.3.47': + resolution: {integrity: sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@4.19.6': - resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} '@types/express@4.17.11': resolution: {integrity: sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg==} @@ -2146,18 +2474,12 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/jsonwebtoken@9.0.2': - resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==} - - '@types/lodash@4.14.168': - resolution: {integrity: sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/minimatch@3.0.5': resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} @@ -2174,18 +2496,24 @@ packages: '@types/morgan@1.9.2': resolution: {integrity: sha512-edtGMEdit146JwwIeyQeHHg9yID4WSolQPxpEorHmN3KuytuCHyn2ELNr5Uxy8SerniFbbkmgKMrGM933am5BQ==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} '@types/node-cron@3.0.11': resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} - '@types/node@18.19.122': - resolution: {integrity: sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA==} + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} '@types/node@20.6.0': resolution: {integrity: sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==} + '@types/node@22.19.9': + resolution: {integrity: sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -2195,8 +2523,8 @@ packages: '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} - '@types/pg@8.15.5': - resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} + '@types/pg@8.16.0': + resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} @@ -2207,20 +2535,20 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/semver@7.7.0': - resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@types/send@0.17.5': - resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - '@types/serve-static@1.15.8': - resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} - '@types/ssh2-streams@0.1.12': - resolution: {integrity: sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==} + '@types/ssh2-streams@0.1.13': + resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} '@types/ssh2@0.5.52': resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} @@ -2243,8 +2571,15 @@ packages: '@types/supertest@2.0.10': resolution: {integrity: sha512-Xt8TbEyZTnD5Xulw95GLMOkmjGICrOQyJ2jqgkSjAUR3mm7pAIzSR0NFBaMcwlzVvlpCjNwbATcWWwjNiZiFrQ==} - '@types/validator@13.15.2': - resolution: {integrity: sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==} + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/uuid@11.0.0': + resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==} + deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed. + + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} @@ -2255,8 +2590,8 @@ packages: '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - '@types/yargs@17.0.33': - resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} '@typescript-eslint/eslint-plugin@6.5.0': resolution: {integrity: sha512-2pktILyjvMaScU6iK3925uvGU87E+N9rh372uGZgiMYwafaw9SXq86U04XPq3UH6tzRvNgBsub6x2DacHc33lw==} @@ -2355,6 +2690,17 @@ packages: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true + abitype@1.2.3: + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -2413,8 +2759,8 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} ansi-styles@3.2.1: @@ -2429,12 +2775,13 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} @@ -2462,10 +2809,6 @@ packages: resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} engines: {node: '>=0.10.0'} - arr-flatten@1.1.0: - resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} - engines: {node: '>=0.10.0'} - arr-union@3.1.0: resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} engines: {node: '>=0.10.0'} @@ -2474,10 +2817,6 @@ packages: resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==} engines: {node: '>=8'} - array-find-index@1.0.2: - resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} - engines: {node: '>=0.10.0'} - array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -2544,18 +2883,24 @@ packages: engines: {node: '>= 4.5.0'} hasBin: true + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.11.0: - resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} - - axios@1.5.1: - resolution: {integrity: sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==} + axios@1.13.5: + resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} - b4a@1.6.7: - resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + b4a@1.7.3: + resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} @@ -2582,19 +2927,28 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - bakosafe@0.2.1-beta.1: - resolution: {integrity: sha512-aGIxLgx2PiO7xH7dtxY8tiTZJrcJUSG4PEeI+55+nvmDKlFIoYJxJ8WupYrlUPDyfOYf6PheWlZbi7/X1fXhzg==} + bakosafe@0.6.0: + resolution: {integrity: sha512-exyZ1NAb4IrTBuvVG2v8+2MaKm3B2Kqe0KZcNFUHtg/7qRrN2gC25m4QNgSlPGkV4cAmPyngWyFSXyV3LP33Mw==} peerDependencies: fuels: ^0.101.0 balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - bare-events@2.6.1: - resolution: {integrity: sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true - bare-fs@4.2.0: - resolution: {integrity: sha512-oRfrw7gwwBVAWx9S5zPMo2iiOjxyiZE12DmblmMQREgcogbNO0AFaZ+QBxxkEXiPspcpvO/Qtqn8LabUx4uYXg==} + bare-fs@4.5.3: + resolution: {integrity: sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==} engines: {bare: '>=1.16.0'} peerDependencies: bare-buffer: '*' @@ -2602,15 +2956,15 @@ packages: bare-buffer: optional: true - bare-os@3.6.1: - resolution: {integrity: sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==} + bare-os@3.6.2: + resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} engines: {bare: '>=1.14.0'} bare-path@3.0.0: resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} - bare-stream@2.6.5: - resolution: {integrity: sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==} + bare-stream@2.7.0: + resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} peerDependencies: bare-buffer: '*' bare-events: '*' @@ -2620,6 +2974,9 @@ packages: bare-events: optional: true + bare-url@2.3.2: + resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2631,6 +2988,10 @@ packages: resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} engines: {node: '>=0.10.0'} + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + basic-auth@2.0.1: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} @@ -2648,16 +3009,16 @@ packages: bn.js@5.2.1: resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} - body-parser@1.19.0: - resolution: {integrity: sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==} - engines: {node: '>= 0.8'} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@1.20.2: - resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} boolbase@1.0.0: @@ -2669,16 +3030,16 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - braces@2.3.2: - resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} - engines: {node: '>=0.10.0'} + brace-expansion@5.0.4: + resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} + engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.25.2: - resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2713,8 +3074,12 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - buildcheck@0.0.6: - resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} + build@0.1.4: + resolution: {integrity: sha512-KwbDJ/zrsU8KZRRMfoURG14cKIAStUlS8D5jBDvtrZbwO5FEkYqc3oB8HIhRiyD64A48w1lc+sOmQ+mmBw5U/Q==} + engines: {node: '>v0.4.12'} + + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} engines: {node: '>=10.0.0'} bull@4.16.5: @@ -2731,10 +3096,6 @@ packages: resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} engines: {node: '>=0.10.0'} - bytes@3.1.0: - resolution: {integrity: sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==} - engines: {node: '>= 0.8'} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -2766,18 +3127,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase-keys@2.1.0: - resolution: {integrity: sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==} - engines: {node: '>=0.10.0'} - camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} engines: {node: '>=8'} - camelcase@2.1.1: - resolution: {integrity: sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==} - engines: {node: '>=0.10.0'} - camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} @@ -2786,11 +3139,11 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001734: - resolution: {integrity: sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==} + caniuse-lite@1.0.30001768: + resolution: {integrity: sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==} - chai@5.2.1: - resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} chalk@2.4.2: @@ -2809,8 +3162,8 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} cheerio-select@2.1.0: @@ -2849,11 +3202,6 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} - cli-highlight@2.1.11: - resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} - hasBin: true - cli-table@0.3.11: resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} engines: {node: '>= 0.2.0'} @@ -2877,8 +3225,8 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - collect-v8-coverage@1.0.2: - resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} collection-visit@1.0.0: resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} @@ -2891,12 +3239,28 @@ packages: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -2919,6 +3283,10 @@ packages: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} @@ -2937,13 +3305,13 @@ packages: engines: {node: '>=18'} hasBin: true - content-disposition@0.5.3: - resolution: {integrity: sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} - engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} @@ -2962,9 +3330,6 @@ packages: engines: {node: '>=10'} hasBin: true - convert-source-map@1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2979,10 +3344,6 @@ packages: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} - cookie@0.4.0: - resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==} - engines: {node: '>= 0.6'} - cookie@0.4.1: resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} engines: {node: '>= 0.6'} @@ -2991,6 +3352,10 @@ packages: resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -3061,9 +3426,9 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} - currently-unhandled@0.4.1: - resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} - engines: {node: '>=0.10.0'} + cssmin@0.3.2: + resolution: {integrity: sha512-bynxGIAJ8ybrnFobjsQotIjA8HFDDgPwbeUWNXXXfR+B4f9kkxdcUyagJoQCSUOfMV+ZZ6bMn8bvbozlCzUGwQ==} + hasBin: true dargs@7.0.0: resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} @@ -3073,12 +3438,11 @@ packages: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} - dateformat@1.0.12: - resolution: {integrity: sha512-5sFRfAAmbHdIts+eKjR9kYJoF0ViCMVX9yqLu5A7S/v+nd077KgCITOMiirmyCBiZpKLDXbBOkYm6tu7rX/TKg==} - hasBin: true + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} - dayjs@1.11.13: - resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} @@ -3105,8 +3469,8 @@ packages: supports-color: optional: true - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -3129,8 +3493,8 @@ packages: dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} - dedent@1.6.0: - resolution: {integrity: sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==} + dedent@1.7.1: + resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: @@ -3172,23 +3536,16 @@ packages: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} - depd@1.1.2: - resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} - engines: {node: '>= 0.6'} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - destroy@1.0.4: - resolution: {integrity: sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==} - destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} detect-newline@3.1.0: @@ -3199,8 +3556,8 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} dir-glob@2.2.2: @@ -3211,16 +3568,16 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - docker-compose@1.2.0: - resolution: {integrity: sha512-wIU1eHk3Op7dFgELRdmOYlPYS4gP8HhH1ZmZa13QZF59y0fblzFDFmKPhyc05phCy2hze9OEvNZAsoljrs+72w==} + docker-compose@1.3.1: + resolution: {integrity: sha512-rF0wH69G3CCcmkN9J1RVMQBaKe8o77LT/3XmqcLIltWWVxcWAzp2TnO7wS3n/umZHN3/EVrlT3exSBMal+Ou1w==} engines: {node: '>= 6.0.0'} docker-modem@5.0.6: resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==} engines: {node: '>= 8.0'} - dockerode@4.0.7: - resolution: {integrity: sha512-R+rgrSRTRdU5mH14PZTCPZtW/zw3HDWNTS/1ZAQpL/5Upe/ye5K9WQkIysu4wBoiMwKynsz0a8qWuGsHgEvSAA==} + dockerode@4.0.9: + resolution: {integrity: sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==} engines: {node: '>= 8.0'} doctrine@3.0.0: @@ -3248,6 +3605,10 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3269,8 +3630,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.200: - resolution: {integrity: sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==} + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -3282,6 +3643,9 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -3316,8 +3680,8 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} @@ -3338,13 +3702,21 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.25.3: resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==} engines: {node: '>=18'} hasBin: true - esbuild@0.25.8: - resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} hasBin: true @@ -3415,8 +3787,8 @@ packages: engines: {node: '>=4'} hasBin: true - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -3449,6 +3821,12 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -3469,8 +3847,8 @@ packages: resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} engines: {node: '>=0.10.0'} - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} expect@29.7.0: @@ -3483,12 +3861,12 @@ packages: peerDependencies: joi: '17' - express@4.17.1: - resolution: {integrity: sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} extend-shallow@2.0.1: @@ -3503,6 +3881,9 @@ packages: resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} engines: {node: '>=0.10.0'} + fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3526,26 +3907,37 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.0.6: - resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: picomatch: optional: true + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -3556,25 +3948,17 @@ packages: filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} - fill-range@4.0.0: - resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} - engines: {node: '>=0.10.0'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@1.1.2: - resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} - engines: {node: '>= 0.8'} - - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} - find-up@1.1.2: - resolution: {integrity: sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==} - engines: {node: '>=0.10.0'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} @@ -3594,6 +3978,9 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -3619,8 +4006,8 @@ packages: resolution: {integrity: sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==} engines: {node: '>= 6'} - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} formidable@1.2.6: @@ -3707,10 +4094,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stdin@4.0.1: - resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==} - engines: {node: '>=0.10.0'} - get-stdin@8.0.0: resolution: {integrity: sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==} engines: {node: '>=10'} @@ -3723,8 +4106,11 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-tsconfig@4.10.1: - resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + get-tsconfig@4.13.3: + resolution: {integrity: sha512-vp8Cj/+9Q/ibZUrq1rhy8mCTQpCk31A3uu9wc1C50yAb3x2pFHOsGdAZQ7jD86ARayyxZUViYeIztW+GE8dcrg==} + + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} @@ -3745,18 +4131,14 @@ packages: glob-to-regexp@0.3.0: resolution: {integrity: sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==} - glob@10.3.15: - resolution: {integrity: sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==} - engines: {node: '>=16 || 14 >=14.18'} - hasBin: true - - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-dirs@0.1.1: resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} @@ -3851,8 +4233,8 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - highlight.js@10.7.3: - resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -3867,18 +4249,14 @@ packages: htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - http-errors@1.7.2: - resolution: {integrity: sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==} - engines: {node: '>= 0.6'} - - http-errors@1.7.3: - resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==} - engines: {node: '>= 0.6'} - http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} @@ -3896,8 +4274,8 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} ieee754@1.2.1: @@ -3915,8 +4293,8 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-in-the-middle@1.14.2: - resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} @@ -3927,10 +4305,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - indent-string@2.1.0: - resolution: {integrity: sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==} - engines: {node: '>=0.10.0'} - indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -3939,17 +4313,14 @@ packages: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - inherits@2.0.3: - resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ioredis@5.7.0: - resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==} + ioredis@5.9.2: + resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==} engines: {node: '>=12.22.0'} ipaddr.js@1.9.1: @@ -4007,10 +4378,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-finite@1.1.0: - resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} - engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -4074,9 +4441,6 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - is-utf8@0.2.1: - resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} - is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -4105,6 +4469,11 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -4125,14 +4494,10 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} - istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} - jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -4290,6 +4655,10 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -4300,6 +4669,11 @@ packages: engines: {node: '>=6'} hasBin: true + jsmin@1.0.1: + resolution: {integrity: sha512-OPuL5X/bFKgVdMvEIX3hnpx3jbVpFCrEM8pKPXjFkZUqg521r41ijdyTz7vACOhW6o1neVlcLyd+wkbK5fNHRg==} + engines: {node: '>=0.1.93'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -4328,8 +4702,8 @@ packages: engines: {node: '>=6'} hasBin: true - jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} jsonify@0.0.1: resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} @@ -4338,15 +4712,19 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} - jsonwebtoken@9.0.1: - resolution: {integrity: sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} - jwa@1.4.2: - resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - jws@3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + jxLoader@0.1.1: + resolution: {integrity: sha512-ClEvAj3K68y8uKhub3RgTmcRPo5DfIWvtxqrKQdDPyZ1UVHIIKvVvjrAsJFSVL5wjv0rt5iH9SMCZ0XRKNzeUA==} + engines: {node: '>v0.4.10'} keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -4370,6 +4748,9 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -4382,8 +4763,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.12.10: - resolution: {integrity: sha512-E91vHJD61jekHHR/RF/E83T/CMoaLXT7cwYA75T4gim4FZjnM6hbJjVIGg7chqlSqRsSvQ3izGmOjHy1SQzcGQ==} + libphonenumber-js@1.12.36: + resolution: {integrity: sha512-woWhKMAVx1fzzUnMCyOzglgSgf6/AFHLASdOBcchYCyvWSGWt12imw3iu2hdI5d4dGZRsNWAmWiz37sDKUPaRQ==} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -4401,10 +4782,6 @@ packages: enquirer: optional: true - load-json-file@1.1.0: - resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==} - engines: {node: '>=0.10.0'} - load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4423,17 +4800,41 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash.partition@4.6.0: + resolution: {integrity: sha512-35L3dSF3Q6V1w5j6V3NhNlQjzsRDC/pYKCTdYTmwqSib+Q8ponkAmt/PwEOq3EmI38DSCl+SkIVwLd+uSlVdrg==} + lodash.truncate@4.4.2: resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} @@ -4443,15 +4844,15 @@ packages: resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} engines: {node: '>=10'} + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - loud-rejection@1.6.0: - resolution: {integrity: sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==} - engines: {node: '>=0.10.0'} - - loupe@3.2.0: - resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4463,12 +4864,12 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - luxon@3.7.1: - resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} engines: {node: '>=12'} - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} @@ -4511,16 +4912,12 @@ packages: memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} - meow@3.7.0: - resolution: {integrity: sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==} - engines: {node: '>=0.10.0'} - meow@8.1.2: resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} engines: {node: '>=10'} - merge-descriptors@1.0.1: - resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} @@ -4557,9 +4954,9 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} @@ -4579,19 +4976,15 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@10.0.3: - resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} - engines: {node: 20 || >=22} + minimatch@3.1.4: + resolution: {integrity: sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + minimatch@5.1.8: + resolution: {integrity: sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==} engines: {node: '>=10'} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + minimatch@9.0.7: + resolution: {integrity: sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==} engines: {node: '>=16 || 14 >=14.17'} minimist-options@4.1.0: @@ -4621,11 +5014,6 @@ packages: engines: {node: '>=10'} hasBin: true - mkdirp@2.1.6: - resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} - engines: {node: '>=10'} - hasBin: true - mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} @@ -4637,8 +5025,8 @@ packages: mongodb-connection-string-url@3.0.2: resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} - mongodb@6.18.0: - resolution: {integrity: sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==} + mongodb@6.21.0: + resolution: {integrity: sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==} engines: {node: '>=16.20.1'} peerDependencies: '@aws-sdk/credential-providers': ^3.188.0 @@ -4646,7 +5034,7 @@ packages: gcp-metadata: ^5.2.0 kerberos: ^2.0.1 mongodb-client-encryption: '>=6.0.0 <7' - snappy: ^7.2.2 + snappy: ^7.3.2 socks: ^2.7.1 peerDependenciesMeta: '@aws-sdk/credential-providers': @@ -4664,6 +5052,10 @@ packages: socks: optional: true + moo-server@1.3.0: + resolution: {integrity: sha512-9A8/eor2DXwpv1+a4pZAAydqLFVrWoKoO1fzdzqLUhYVXAO1Kgd1FR2gFZi7YdHzF0s4W8cDNwCfKJQrvLqxDw==} + engines: {node: '>v0.4.10'} + morgan@1.10.0: resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} engines: {node: '>= 0.8.0'} @@ -4675,9 +5067,6 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.1: - resolution: {integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -4685,18 +5074,19 @@ packages: resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} hasBin: true - msgpackr@1.11.5: - resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + msgpackr@1.11.8: + resolution: {integrity: sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==} multimatch@4.0.0: resolution: {integrity: sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==} engines: {node: '>=8'} - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + mylas@2.1.14: + resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==} + engines: {node: '>=16.0.0'} - nan@2.23.0: - resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==} + nan@2.25.0: + resolution: {integrity: sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} @@ -4721,8 +5111,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-abi@3.75.0: - resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} engines: {node: '>=10'} node-cron@3.0.3: @@ -4745,11 +5135,11 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - nodemailer@6.9.8: - resolution: {integrity: sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ==} + nodemailer@8.0.1: + resolution: {integrity: sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==} engines: {node: '>=6.0.0'} noms@0.0.0: @@ -4773,8 +5163,8 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} - npm@10.9.3: - resolution: {integrity: sha512-6Eh1u5Q+kIVXeA8e7l2c/HpnFFcwrkt37xDMujD5be1gloWa9p6j3Fsv3mByXXmqJHy+2cElRMML8opNT7xIJQ==} + npm@10.9.4: + resolution: {integrity: sha512-OnUG836FwboQIbqtefDNlyR0gTHzIfwRfE3DuiNewBvnMnWEpB0VEXwBlFVgqpNzIgYo/MHh3d2Hel/pszapAA==} engines: {node: ^18.17.0 || >=20.5.0} hasBin: true bundledDependencies: @@ -4874,6 +5264,10 @@ packages: resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} engines: {node: '>=0.10.0'} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -4889,6 +5283,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -4905,6 +5302,14 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + ox@0.11.3: + resolution: {integrity: sha512-1bWYGk/xZel3xro3l8WGg6eq4YEKlaqvyMtVhfMFpbJzK2F6rj4EDRtqDCWVEJMkzcmEi9uW2QxsqELokOlarw==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -4939,26 +5344,13 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parse-json@2.2.0: - resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==} - engines: {node: '>=0.10.0'} - parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse5-htmlparser2-tree-adapter@6.0.1: - resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} - parse5-htmlparser2-tree-adapter@7.1.0: resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} - parse5@5.1.1: - resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} - - parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -4978,10 +5370,6 @@ packages: path-dirname@1.0.2: resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} - path-exists@2.1.0: - resolution: {integrity: sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==} - engines: {node: '>=0.10.0'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -5001,16 +5389,11 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@0.1.7: - resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - path-to-regexp@8.2.0: - resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} - engines: {node: '>=16'} - - path-type@1.1.0: - resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==} - engines: {node: '>=0.10.0'} + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} @@ -5027,20 +5410,20 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} - pg-connection-string@2.9.1: - resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + pg-connection-string@2.11.0: + resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - pg-pool@3.10.1: - resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} + pg-pool@3.11.0: + resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==} peerDependencies: pg: '>=8.0' - pg-protocol@1.10.3: - resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + pg-protocol@1.11.0: + resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} @@ -5069,10 +5452,6 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - pify@3.0.0: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} @@ -5081,13 +5460,22 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - pinkie-promise@2.0.1: - resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} - engines: {node: '>=0.10.0'} + pino-abstract-transport@1.2.0: + resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} - pinkie@2.0.4: - resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} - engines: {node: '>=0.10.0'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-pretty@11.2.2: + resolution: {integrity: sha512-2FnyGir8nAJAqD3srROdrF1J5BIcMT4nwj7hHSc60El6Uxlym00UbCCd8pYIterstVBFlMyF1yFV8XdGIPbj4A==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@9.6.0: + resolution: {integrity: sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==} + hasBin: true pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} @@ -5100,6 +5488,15 @@ packages: please-upgrade-node@3.2.0: resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + + pnpm@10.28.2: + resolution: {integrity: sha512-QYcvA3rSL3NI47Heu69+hnz9RI8nJtnPdMCPGVB8MdLI56EVJbmD/rwt9kC1Q43uYCPrsfhO1DzC1lTSvDJiZA==} + engines: {node: '>=18.12'} + hasBin: true + portfinder@1.0.32: resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} engines: {node: '>= 0.12.0'} @@ -5120,8 +5517,8 @@ packages: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} - postgres-bytea@1.0.0: - resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} engines: {node: '>=0.10.0'} postgres-date@1.0.7: @@ -5136,8 +5533,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} prettier@2.2.1: @@ -5159,6 +5556,9 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -5167,6 +5567,9 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} + promised-io@0.3.6: + resolution: {integrity: sha512-bNwZusuNIW4m0SPR8jooSyndD35ggirHlxVl/UhIaZD/F0OBv9ebfc6tNmbpZts3QXHggkjIBH8lvtnzhtcz0A==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -5181,8 +5584,12 @@ packages: property-expr@2.0.6: resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} - protobufjs@7.5.3: - resolution: {integrity: sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + protobufjs@8.0.0: + resolution: {integrity: sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -5210,25 +5617,23 @@ packages: (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) - qs@6.11.0: - resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} - qs@6.12.1: - resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==} - engines: {node: '>=0.6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} - engines: {node: '>=0.6'} - - qs@6.7.0: - resolution: {integrity: sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==} - engines: {node: '>=0.6'} + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + quick-lru@4.0.1: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} @@ -5240,33 +5645,25 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@2.4.0: - resolution: {integrity: sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==} - engines: {node: '>= 0.8'} - raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - read-pkg-up@1.0.1: - resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==} - engines: {node: '>=0.10.0'} - read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} - read-pkg@1.1.0: - resolution: {integrity: sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==} - engines: {node: '>=0.10.0'} - read-pkg@5.2.0: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} @@ -5292,9 +5689,9 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - redent@1.0.0: - resolution: {integrity: sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==} - engines: {node: '>=0.10.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} @@ -5328,18 +5725,6 @@ packages: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} engines: {node: '>=8'} - repeat-element@1.1.4: - resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} - engines: {node: '>=0.10.0'} - - repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} - - repeating@2.0.1: - resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==} - engines: {node: '>=0.10.0'} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -5352,6 +5737,9 @@ packages: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -5379,8 +5767,8 @@ packages: resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} engines: {node: '>=10'} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} hasBin: true @@ -5417,8 +5805,8 @@ packages: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true - rollup@4.46.2: - resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -5441,9 +5829,16 @@ packages: safe-regex@1.1.0: resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + semver-compare@1.0.0: resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} @@ -5455,30 +5850,30 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.3.5: - resolution: {integrity: sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==} + semver@7.5.2: + resolution: {integrity: sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==} engines: {node: '>=10'} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true - send@0.17.1: - resolution: {integrity: sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==} + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serve-static@1.14.1: - resolution: {integrity: sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==} + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} set-function-length@1.2.2: @@ -5489,9 +5884,6 @@ packages: resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} engines: {node: '>=0.10.0'} - setprototypeof@1.1.1: - resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -5560,33 +5952,28 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} - snapdragon-node@2.1.1: - resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} - engines: {node: '>=0.10.0'} - - snapdragon-util@3.0.1: - resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} - engines: {node: '>=0.10.0'} - snapdragon@0.8.2: resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} engines: {node: '>=0.10.0'} - socket.io-adapter@2.5.5: - resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + socket.io-adapter@2.5.6: + resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==} socket.io-client@4.7.5: resolution: {integrity: sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==} engines: {node: '>=10.0.0'} - socket.io-parser@4.2.4: - resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + socket.io-parser@4.2.5: + resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} engines: {node: '>=10.0.0'} socket.io@4.7.2: resolution: {integrity: sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==} engines: {node: '>=10.2.0'} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -5645,13 +6032,20 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sql-highlight@6.1.0: + resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==} + engines: {node: '>=14'} + ssh-remote-port-forward@1.0.4: resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} - ssh2@1.16.0: - resolution: {integrity: sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==} + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} engines: {node: '>=10.16.0'} + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -5666,10 +6060,6 @@ packages: resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} engines: {node: '>=0.10.0'} - statuses@1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} - statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -5678,11 +6068,11 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@3.9.0: - resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - streamx@2.22.1: - resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} string-argv@0.3.1: resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} @@ -5717,14 +6107,10 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} - strip-bom@2.0.0: - resolution: {integrity: sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==} - engines: {node: '>=0.10.0'} - strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -5737,11 +6123,6 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - strip-indent@1.0.1: - resolution: {integrity: sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==} - engines: {node: '>=0.10.0'} - hasBin: true - strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -5780,15 +6161,18 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svix@1.76.1: + resolution: {integrity: sha512-CRuDWBTgYfDnBLRaZdKp9VuoPcNUq9An14c/k+4YJ15Qc5Grvf66vp0jvTltd4t7OIRj+8lM1DAgvSgvf7hdLw==} + table@6.9.0: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tar-fs@2.1.3: - resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==} + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} - tar-fs@3.1.0: - resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} + tar-fs@3.1.1: + resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} @@ -5801,8 +6185,8 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - testcontainers@11.5.1: - resolution: {integrity: sha512-YSSP4lSJB8498zTeu4HYTZYgSky54ozBmIDdC8PFU5inj+vBo5hPpilhcYTgmsqsYjrXOJGV7jl0MWByS7GwuA==} + testcontainers@11.11.0: + resolution: {integrity: sha512-nKTJn3n/gkyGg/3SVkOwX+isPOGSHlfI+CWMobSmvQrsj7YW01aWvl2pYIfV4LMd+C8or783yYrzKSK2JlP+Qw==} text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} @@ -5811,15 +6195,14 @@ packages: resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} engines: {node: '>=0.10'} + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} @@ -5830,6 +6213,10 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + timespan@2.3.0: + resolution: {integrity: sha512-0Jq9+58T2wbOyLth0EU+AUb6JMGCLaTWIykJFa7hyAybjVH9gpVMTfUAwo5fWAvtFt2Tjh/Elg8JtgNpnMnM8g==} + engines: {node: '>= 0.2.0'} + tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} @@ -5839,8 +6226,8 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} tinypool@1.1.1: @@ -5866,8 +6253,8 @@ packages: tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - to-buffer@1.2.1: - resolution: {integrity: sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==} + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} engines: {node: '>= 0.4'} to-fast-properties@2.0.0: @@ -5878,10 +6265,6 @@ packages: resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} engines: {node: '>=0.10.0'} - to-regex-range@2.1.1: - resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} - engines: {node: '>=0.10.0'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -5890,10 +6273,6 @@ packages: resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} engines: {node: '>=0.10.0'} - toidentifier@1.0.0: - resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==} - engines: {node: '>=0.6'} - toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -5915,22 +6294,22 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - trim-newlines@1.0.0: - resolution: {integrity: sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==} - engines: {node: '>=0.10.0'} - trim-newlines@3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' - ts-jest@29.4.1: - resolution: {integrity: sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==} + ts-jest@29.4.6: + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -5956,8 +6335,8 @@ packages: jest-util: optional: true - ts-node-dev@1.1.6: - resolution: {integrity: sha512-RTUi7mHMNQospArGz07KiraQcdgUVNXKsgO2HAi7FoiyPMdTDqdniB6K1dqyaIxT7c9v/VpSbfBZPS6uVpaFLQ==} + ts-node-dev@2.0.0: + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} engines: {node: '>=0.8.0'} hasBin: true peerDependencies: @@ -5981,12 +6360,10 @@ packages: '@swc/wasm': optional: true - ts-node@9.1.1: - resolution: {integrity: sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==} - engines: {node: '>=10.0.0'} + tsc-alias@1.8.16: + resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==} + engines: {node: '>=16.20.2'} hasBin: true - peerDependencies: - typescript: '>=2.7' tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -6001,8 +6378,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.19.3: - resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} hasBin: true @@ -6095,28 +6472,27 @@ packages: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} - typeorm@0.3.20: - resolution: {integrity: sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==} + typeorm@0.3.28: + resolution: {integrity: sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==} engines: {node: '>=16.13.0'} hasBin: true peerDependencies: - '@google-cloud/spanner': ^5.18.0 - '@sap/hana-client': ^2.12.25 - better-sqlite3: ^7.1.2 || ^8.0.0 || ^9.0.0 - hdb-pool: ^0.1.6 + '@google-cloud/spanner': ^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@sap/hana-client': ^2.14.22 + better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 ioredis: ^5.0.4 - mongodb: ^5.8.0 - mssql: ^9.1.1 || ^10.0.1 + mongodb: ^5.8.0 || ^6.0.0 + mssql: ^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0 mysql2: ^2.2.5 || ^3.0.1 oracledb: ^6.3.0 pg: ^8.5.1 pg-native: ^3.0.0 pg-query-stream: ^4.0.0 - redis: ^3.1.1 || ^4.0.0 + redis: ^3.1.1 || ^4.0.0 || ^5.0.14 sql.js: ^1.4.0 sqlite3: ^5.0.3 ts-node: ^10.7.0 - typeorm-aurora-data-api-driver: ^2.0.0 + typeorm-aurora-data-api-driver: ^2.0.0 || ^3.0.0 peerDependenciesMeta: '@google-cloud/spanner': optional: true @@ -6124,8 +6500,6 @@ packages: optional: true better-sqlite3: optional: true - hdb-pool: - optional: true ioredis: optional: true mongodb: @@ -6170,8 +6544,11 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici@7.13.0: - resolution: {integrity: sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.20.0: + resolution: {integrity: sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==} engines: {node: '>=20.18.1'} union-value@1.0.1: @@ -6194,8 +6571,8 @@ packages: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -6207,6 +6584,9 @@ packages: resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} deprecated: Please see https://github.com/lydell/urix#deprecated + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use@3.1.1: resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} engines: {node: '>=0.10.0'} @@ -6222,6 +6602,14 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -6243,21 +6631,29 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - validator@13.15.15: - resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} + validator@13.15.26: + resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} engines: {node: '>= 0.10'} vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + viem@2.45.1: + resolution: {integrity: sha512-LN6Pp7vSfv50LgwhkfSbIXftAM5J89lP9x8TeDa8QM7o41IxlHrDh0F9X+FfnCWtsz11pEVV5sn+yBUoOHNqYA==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + vite-node@3.0.9: resolution: {integrity: sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@6.3.5: - resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -6346,8 +6742,8 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} which@2.0.2: @@ -6365,6 +6761,14 @@ packages: engines: {node: '>=20.11'} hasBin: true + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -6387,12 +6791,29 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + wrench@1.3.9: + resolution: {integrity: sha512-srTJQmLTP5YtW+F5zDuqjMEZqLLr/eJOZfDI5ibfPfRMeDh3oBUefAscuH0q5wBKE339ptH/S/0D18ZkfOfmKQ==} + engines: {node: '>=0.1.97'} + deprecated: wrench.js is deprecated! You should check out fs-extra (https://github.com/jprichardson/node-fs-extra) for any operations you were using wrench for. Thanks for all the usage over the years. + write-file-atomic@4.0.2: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - ws@8.17.1: - resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -6425,8 +6846,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true @@ -6468,301 +6889,274 @@ packages: snapshots: - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.30 + '@adraffy/ens-normalize@1.11.1': {} '@babel/code-frame@7.12.11': dependencies: '@babel/highlight': 7.25.9 - '@babel/code-frame@7.27.1': + '@babel/code-frame@7.29.0': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.28.0': {} - - '@babel/core@7.13.10': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.13.9 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.13.10) - '@babel/helpers': 7.28.2 - '@babel/parser': 7.13.10 - '@babel/template': 7.27.2 - '@babel/traverse': 7.13.0 - '@babel/types': 7.13.0 - convert-source-map: 1.9.0 - debug: 4.4.1 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - lodash: 4.17.21 - semver: 6.3.1 - source-map: 0.5.7 - transitivePeerDependencies: - - supports-color + '@babel/compat-data@7.29.0': {} - '@babel/core@7.28.0': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 - '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.28.2 - '@babel/parser': 7.28.0 - '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.2 + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/generator@7.13.9': + '@babel/generator@7.17.7': dependencies: - '@babel/types': 7.13.0 + '@babel/types': 7.17.0 jsesc: 2.5.2 source-map: 0.5.7 - '@babel/generator@7.28.0': + '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.28.0 - '@babel/types': 7.28.2 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.27.2': + '@babel/helper-compilation-targets@7.28.6': dependencies: - '@babel/compat-data': 7.28.0 + '@babel/compat-data': 7.29.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.2 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-environment-visitor@7.24.7': + dependencies: + '@babel/types': 7.29.0 + '@babel/helper-function-name@7.24.7': dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 '@babel/helper-globals@7.28.0': {} - '@babel/helper-module-imports@7.27.1': + '@babel/helper-hoist-variables@7.24.7': dependencies: - '@babel/traverse': 7.28.0 - '@babel/types': 7.28.2 - transitivePeerDependencies: - - supports-color + '@babel/types': 7.29.0 - '@babel/helper-module-transforms@7.27.3(@babel/core@7.13.10)': + '@babel/helper-module-imports@7.28.6': dependencies: - '@babel/core': 7.13.10 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-plugin-utils@7.28.6': {} '@babel/helper-split-export-declaration@7.24.7': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.29.0 '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.28.2': + '@babel/helpers@7.28.6': dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 '@babel/highlight@7.25.9': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 chalk: 2.4.2 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/parser@7.13.10': - dependencies: - '@babel/types': 7.13.0 - - '@babel/parser@7.28.0': + '@babel/parser@7.29.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.29.0 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.0)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.0)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.0)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.0)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.0)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.0)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.0)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.0)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.0)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.0)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.0)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.0)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.0)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.0)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.28.0 - '@babel/helper-plugin-utils': 7.27.1 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/runtime@7.28.2': {} + '@babel/runtime@7.28.6': {} - '@babel/template@7.27.2': + '@babel/template@7.28.6': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.0 - '@babel/types': 7.28.2 + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 - '@babel/traverse@7.13.0': + '@babel/traverse@7.23.2': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.13.9 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-environment-visitor': 7.24.7 '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 '@babel/helper-split-export-declaration': 7.24.7 - '@babel/parser': 7.13.10 - '@babel/types': 7.13.0 - debug: 4.4.1 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + debug: 4.4.3 globals: 11.12.0 - lodash: 4.17.21 transitivePeerDependencies: - supports-color - '@babel/traverse@7.28.0': + '@babel/traverse@7.29.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.0 - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 - debug: 4.4.1 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.13.0': + '@babel/types@7.17.0': dependencies: - '@babel/helper-validator-identifier': 7.27.1 - lodash: 4.17.21 + '@babel/helper-validator-identifier': 7.28.5 to-fast-properties: 2.0.0 - '@babel/types@7.28.2': + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@balena/dockerignore@1.0.2': {} '@bcoe/v8-coverage@0.2.3': {} - '@bull-board/api@6.12.0(@bull-board/ui@6.12.0)': + '@bull-board/api@6.16.4(@bull-board/ui@6.16.4)': dependencies: - '@bull-board/ui': 6.12.0 + '@bull-board/ui': 6.16.4 redis-info: 3.1.0 - '@bull-board/express@6.12.0': + '@bull-board/express@6.16.4': dependencies: - '@bull-board/api': 6.12.0(@bull-board/ui@6.12.0) - '@bull-board/ui': 6.12.0 + '@bull-board/api': 6.16.4(@bull-board/ui@6.16.4) + '@bull-board/ui': 6.16.4 ejs: 3.1.10 - express: 5.1.0 + express: 5.2.1 transitivePeerDependencies: - supports-color - '@bull-board/ui@6.12.0': + '@bull-board/ui@6.16.4': dependencies: - '@bull-board/api': 6.12.0(@bull-board/ui@6.12.0) + '@bull-board/api': 6.16.4(@bull-board/ui@6.16.4) + + '@colors/colors@1.6.0': {} '@commitlint/cli@12.0.1': dependencies: @@ -6772,7 +7166,7 @@ snapshots: '@commitlint/read': 12.1.4 '@commitlint/types': 12.1.4 get-stdin: 8.0.0 - lodash: 4.17.21 + lodash: 4.17.23 resolve-from: 5.0.0 resolve-global: 1.0.0 yargs: 16.2.0 @@ -6784,7 +7178,7 @@ snapshots: '@commitlint/ensure@12.1.4': dependencies: '@commitlint/types': 12.1.4 - lodash: 4.17.21 + lodash: 4.17.23 '@commitlint/execute-rule@12.1.4': {} @@ -6796,7 +7190,7 @@ snapshots: '@commitlint/is-ignored@12.1.4': dependencies: '@commitlint/types': 12.1.4 - semver: 7.3.5 + semver: 7.5.2 '@commitlint/lint@12.1.4': dependencies: @@ -6812,7 +7206,7 @@ snapshots: '@commitlint/types': 12.1.4 chalk: 4.1.2 cosmiconfig: 7.1.0 - lodash: 4.17.21 + lodash: 4.17.23 resolve-from: 5.0.0 '@commitlint/message@12.1.4': {} @@ -6833,7 +7227,7 @@ snapshots: '@commitlint/resolve-extends@12.1.4': dependencies: import-fresh: 3.3.1 - lodash: 4.17.21 + lodash: 4.17.23 resolve-from: 5.0.0 resolve-global: 1.0.0 @@ -6858,6 +7252,12 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + '@envio-dev/hypersync-client-darwin-arm64@0.6.2': optional: true @@ -6878,7 +7278,7 @@ snapshots: '@envio-dev/hypersync-client@0.6.2': dependencies: - npm: 10.9.3 + npm: 10.9.4 yarn: 1.22.22 optionalDependencies: '@envio-dev/hypersync-client-darwin-arm64': 0.6.2 @@ -6888,176 +7288,254 @@ snapshots: '@envio-dev/hypersync-client-linux-x64-musl': 0.6.2 '@envio-dev/hypersync-client-win32-x64-msvc': 0.6.2 + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/aix-ppc64@0.25.3': optional: true - '@esbuild/aix-ppc64@0.25.8': + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.25.12': optional: true '@esbuild/android-arm64@0.25.3': optional: true - '@esbuild/android-arm64@0.25.8': + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.25.12': optional: true '@esbuild/android-arm@0.25.3': optional: true - '@esbuild/android-arm@0.25.8': + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.25.12': optional: true '@esbuild/android-x64@0.25.3': optional: true - '@esbuild/android-x64@0.25.8': + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.25.12': optional: true '@esbuild/darwin-arm64@0.25.3': optional: true - '@esbuild/darwin-arm64@0.25.8': + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.25.12': optional: true '@esbuild/darwin-x64@0.25.3': optional: true - '@esbuild/darwin-x64@0.25.8': + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': optional: true '@esbuild/freebsd-arm64@0.25.3': optional: true - '@esbuild/freebsd-arm64@0.25.8': + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.25.12': optional: true '@esbuild/freebsd-x64@0.25.3': optional: true - '@esbuild/freebsd-x64@0.25.8': + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.25.12': optional: true '@esbuild/linux-arm64@0.25.3': optional: true - '@esbuild/linux-arm64@0.25.8': + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.25.12': optional: true '@esbuild/linux-arm@0.25.3': optional: true - '@esbuild/linux-arm@0.25.8': + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.25.12': optional: true '@esbuild/linux-ia32@0.25.3': optional: true - '@esbuild/linux-ia32@0.25.8': + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.25.12': optional: true '@esbuild/linux-loong64@0.25.3': optional: true - '@esbuild/linux-loong64@0.25.8': + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.25.12': optional: true '@esbuild/linux-mips64el@0.25.3': optional: true - '@esbuild/linux-mips64el@0.25.8': + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.25.12': optional: true '@esbuild/linux-ppc64@0.25.3': optional: true - '@esbuild/linux-ppc64@0.25.8': + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.25.12': optional: true '@esbuild/linux-riscv64@0.25.3': optional: true - '@esbuild/linux-riscv64@0.25.8': + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.25.12': optional: true '@esbuild/linux-s390x@0.25.3': optional: true - '@esbuild/linux-s390x@0.25.8': + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.25.12': optional: true '@esbuild/linux-x64@0.25.3': optional: true - '@esbuild/linux-x64@0.25.8': + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': optional: true '@esbuild/netbsd-arm64@0.25.3': optional: true - '@esbuild/netbsd-arm64@0.25.8': + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.25.12': optional: true '@esbuild/netbsd-x64@0.25.3': optional: true - '@esbuild/netbsd-x64@0.25.8': + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': optional: true '@esbuild/openbsd-arm64@0.25.3': optional: true - '@esbuild/openbsd-arm64@0.25.8': + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.25.12': optional: true '@esbuild/openbsd-x64@0.25.3': optional: true - '@esbuild/openbsd-x64@0.25.8': + '@esbuild/openbsd-x64@0.27.3': optional: true - '@esbuild/openharmony-arm64@0.25.8': + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.25.12': optional: true '@esbuild/sunos-x64@0.25.3': optional: true - '@esbuild/sunos-x64@0.25.8': + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.25.12': optional: true '@esbuild/win32-arm64@0.25.3': optional: true - '@esbuild/win32-arm64@0.25.8': + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.25.12': optional: true '@esbuild/win32-ia32@0.25.3': optional: true - '@esbuild/win32-ia32@0.25.8': + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.25.12': optional: true '@esbuild/win32-x64@0.25.3': optional: true - '@esbuild/win32-x64@0.25.8': + '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@7.22.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@7.22.0)': dependencies: eslint: 7.22.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} '@eslint/eslintrc@0.4.3': dependencies: ajv: 6.12.6 - debug: 4.4.1 + debug: 4.4.3 espree: 7.3.1 globals: 13.24.0 ignore: 4.0.6 import-fresh: 3.3.1 - js-yaml: 3.14.1 - minimatch: 3.1.2 + js-yaml: 3.14.2 + minimatch: 3.1.4 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color @@ -7075,24 +7553,24 @@ snapshots: '@ethersproject/logger@5.8.0': {} - '@fuel-ts/abi-coder@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/abi-coder@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/errors': 0.101.3 - '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/math': 0.101.3 - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) type-fest: 4.34.1 transitivePeerDependencies: - vitest - '@fuel-ts/abi-typegen@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/abi-typegen@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@fuel-ts/errors': 0.101.3 - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/versions': 0.101.3 commander: 13.1.0 - glob: 10.4.5 + glob: 10.5.0 handlebars: 4.7.8 mkdirp: 3.0.1 ramda: 0.30.1 @@ -7100,17 +7578,17 @@ snapshots: transitivePeerDependencies: - vitest - '@fuel-ts/account@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/account@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/address': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/address': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/errors': 0.101.3 - '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/math': 0.101.3 - '@fuel-ts/merkle': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/merkle': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/versions': 0.101.3 '@fuels/vm-asm': 0.60.2 '@noble/curves': 1.8.1 @@ -7123,37 +7601,37 @@ snapshots: - encoding - vitest - '@fuel-ts/address@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/address@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/errors': 0.101.3 - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@noble/hashes': 1.7.1 transitivePeerDependencies: - vitest - '@fuel-ts/contract@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/contract@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/account': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/account': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/errors': 0.101.3 - '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/math': 0.101.3 - '@fuel-ts/merkle': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/program': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/merkle': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/program': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuels/vm-asm': 0.60.2 ramda: 0.30.1 transitivePeerDependencies: - encoding - vitest - '@fuel-ts/crypto@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/crypto@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@fuel-ts/errors': 0.101.3 - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@noble/hashes': 1.7.1 transitivePeerDependencies: - vitest @@ -7162,10 +7640,10 @@ snapshots: dependencies: '@fuel-ts/versions': 0.101.3 - '@fuel-ts/hasher@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/hasher@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@noble/hashes': 1.7.1 transitivePeerDependencies: - vitest @@ -7176,73 +7654,73 @@ snapshots: '@types/bn.js': 5.1.6 bn.js: 5.2.1 - '@fuel-ts/merkle@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/merkle@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/math': 0.101.3 transitivePeerDependencies: - vitest - '@fuel-ts/program@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/program@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/account': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/address': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/account': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/address': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/errors': 0.101.3 '@fuel-ts/math': 0.101.3 - '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuels/vm-asm': 0.60.2 ramda: 0.30.1 transitivePeerDependencies: - encoding - vitest - '@fuel-ts/recipes@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/recipes@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/abi-typegen': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/account': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/address': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/contract': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/program': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/abi-typegen': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/account': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/address': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/contract': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/program': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - encoding - vitest - '@fuel-ts/script@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/script@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/account': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/account': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/errors': 0.101.3 '@fuel-ts/math': 0.101.3 - '@fuel-ts/program': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/program': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - encoding - vitest - '@fuel-ts/transactions@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/transactions@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/address': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/address': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/errors': 0.101.3 - '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/math': 0.101.3 - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - vitest - '@fuel-ts/utils@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@fuel-ts/utils@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@fuel-ts/errors': 0.101.3 '@fuel-ts/math': 0.101.3 '@fuel-ts/versions': 0.101.3 fflate: 0.8.2 - vitest: 3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1) + vitest: 3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2) '@fuel-ts/versions@0.101.3': dependencies: @@ -7255,16 +7733,23 @@ snapshots: dependencies: graphql: 16.10.0 - '@grpc/grpc-js@1.13.4': + '@grpc/grpc-js@1.14.3': dependencies: - '@grpc/proto-loader': 0.7.15 + '@grpc/proto-loader': 0.8.0 '@js-sdsl/ordered-map': 4.4.2 '@grpc/proto-loader@0.7.15': dependencies: lodash.camelcase: 4.3.0 long: 5.3.2 - protobufjs: 7.5.3 + protobufjs: 7.5.4 + yargs: 17.7.2 + + '@grpc/proto-loader@0.8.0': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 yargs: 17.7.2 '@hapi/hoek@9.3.0': {} @@ -7273,19 +7758,13 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 - '@ioredis/commands@1.3.0': {} - - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 + '@ioredis/commands@1.5.0': {} '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -7295,7 +7774,7 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.14.1 + js-yaml: 3.14.2 resolve-from: 5.0.0 '@istanbuljs/schema@0.1.3': {} @@ -7387,10 +7866,10 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 '@types/node': 20.6.0 chalk: 4.1.2 - collect-v8-coverage: 1.0.2 + collect-v8-coverage: 1.0.3 exit: 0.1.2 glob: 7.2.3 graceful-fs: 4.2.11 @@ -7398,7 +7877,7 @@ snapshots: istanbul-lib-instrument: 6.0.3 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.7 + istanbul-reports: 3.2.0 jest-message-util: 29.7.0 jest-util: 29.7.0 jest-worker: 29.7.0 @@ -7411,11 +7890,11 @@ snapshots: '@jest/schemas@29.6.3': dependencies: - '@sinclair/typebox': 0.27.8 + '@sinclair/typebox': 0.27.10 '@jest/source-map@29.6.3': dependencies: - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 @@ -7424,7 +7903,7 @@ snapshots: '@jest/console': 29.7.0 '@jest/types': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.2 + collect-v8-coverage: 1.0.3 '@jest/test-sequencer@29.7.0': dependencies: @@ -7435,9 +7914,9 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 6.1.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -7459,19 +7938,24 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 '@types/node': 20.6.0 - '@types/yargs': 17.0.33 + '@types/yargs': 17.0.35 chalk: 4.1.2 '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.30': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 @@ -7483,7 +7967,7 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} - '@mongodb-js/saslprep@1.3.0': + '@mongodb-js/saslprep@1.4.5': dependencies: sparse-bitfield: 3.0.3 @@ -7510,6 +7994,8 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true + '@noble/ciphers@1.3.0': {} + '@noble/curves@1.3.0': dependencies: '@noble/hashes': 1.3.3 @@ -7522,12 +8008,24 @@ snapshots: dependencies: '@noble/hashes': 1.7.1 + '@noble/curves@1.9.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + '@noble/hashes@1.3.3': {} '@noble/hashes@1.4.0': {} '@noble/hashes@1.7.1': {} + '@noble/hashes@1.8.0': {} + + '@noble/secp256k1@2.3.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7540,12 +8038,16 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + fastq: 1.20.1 '@opentelemetry/api-logs@0.201.1': dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.212.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.52.1': dependencies: '@opentelemetry/api': 1.9.0 @@ -7579,9 +8081,19 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.34.0 + + '@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/exporter-logs-otlp-grpc@0.201.1(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.13.4 + '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.201.1(@opentelemetry/api@1.9.0) @@ -7611,7 +8123,7 @@ snapshots: '@opentelemetry/exporter-metrics-otlp-grpc@0.201.1(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.13.4 + '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-metrics-otlp-http': 0.201.1(@opentelemetry/api@1.9.0) @@ -7649,7 +8161,7 @@ snapshots: '@opentelemetry/exporter-trace-otlp-grpc@0.201.1(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.13.4 + '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.201.1(@opentelemetry/api@1.9.0) @@ -7676,6 +8188,15 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -7722,7 +8243,7 @@ snapshots: '@opentelemetry/instrumentation-express@0.50.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.201.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.34.0 transitivePeerDependencies: @@ -7784,7 +8305,7 @@ snapshots: '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.27.0 - semver: 7.7.2 + semver: 7.7.4 transitivePeerDependencies: - supports-color @@ -7881,7 +8402,7 @@ snapshots: '@opentelemetry/instrumentation-typeorm@0.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.201.1(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -7899,7 +8420,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.201.1 '@types/shimmer': 1.2.0 - import-in-the-middle: 1.14.2 + import-in-the-middle: 1.15.0 require-in-the-middle: 7.5.2 shimmer: 1.2.1 transitivePeerDependencies: @@ -7910,9 +8431,9 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.52.1 '@types/shimmer': 1.2.0 - import-in-the-middle: 1.14.2 + import-in-the-middle: 1.15.0 require-in-the-middle: 7.5.2 - semver: 7.7.2 + semver: 7.7.4 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -7922,9 +8443,9 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.53.0 '@types/shimmer': 1.2.0 - import-in-the-middle: 1.14.2 + import-in-the-middle: 1.15.0 require-in-the-middle: 7.5.2 - semver: 7.7.2 + semver: 7.7.4 shimmer: 1.2.1 transitivePeerDependencies: - supports-color @@ -7935,9 +8456,15 @@ snapshots: '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.201.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base@0.201.1(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.13.4 + '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-exporter-base': 0.201.1(@opentelemetry/api@1.9.0) @@ -7952,7 +8479,18 @@ snapshots: '@opentelemetry/sdk-logs': 0.201.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) - protobufjs: 7.5.3 + protobufjs: 7.5.4 + + '@opentelemetry/otlp-transformer@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + protobufjs: 8.0.0 '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0)': dependencies: @@ -7978,6 +8516,12 @@ snapshots: '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/sdk-logs@0.201.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -7985,6 +8529,13 @@ snapshots: '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -7997,6 +8548,12 @@ snapshots: '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node@0.201.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -8039,6 +8596,13 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.34.0 + '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -8119,79 +8683,107 @@ snapshots: dependencies: '@redis/client': 1.6.0 - '@rollup/rollup-android-arm-eabi@4.46.2': + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': optional: true - '@rollup/rollup-android-arm64@4.46.2': + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': optional: true - '@rollup/rollup-darwin-arm64@4.46.2': + '@rollup/rollup-linux-arm-musleabihf@4.59.0': optional: true - '@rollup/rollup-darwin-x64@4.46.2': + '@rollup/rollup-linux-arm64-gnu@4.59.0': optional: true - '@rollup/rollup-freebsd-arm64@4.46.2': + '@rollup/rollup-linux-arm64-musl@4.59.0': optional: true - '@rollup/rollup-freebsd-x64@4.46.2': + '@rollup/rollup-linux-loong64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + '@rollup/rollup-linux-loong64-musl@4.59.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.46.2': + '@rollup/rollup-linux-ppc64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.46.2': + '@rollup/rollup-linux-ppc64-musl@4.59.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.46.2': + '@rollup/rollup-linux-riscv64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + '@rollup/rollup-linux-riscv64-musl@4.59.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.46.2': + '@rollup/rollup-linux-s390x-gnu@4.59.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.46.2': + '@rollup/rollup-linux-x64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.46.2': + '@rollup/rollup-linux-x64-musl@4.59.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.46.2': + '@rollup/rollup-openbsd-x64@4.59.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.46.2': + '@rollup/rollup-openharmony-arm64@4.59.0': optional: true - '@rollup/rollup-linux-x64-musl@4.46.2': + '@rollup/rollup-win32-arm64-msvc@4.59.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.46.2': + '@rollup/rollup-win32-ia32-msvc@4.59.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.46.2': + '@rollup/rollup-win32-x64-gnu@4.59.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.46.2': + '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true '@scure/base@1.1.9': {} + '@scure/base@1.2.6': {} + '@scure/bip32@1.4.0': dependencies: '@noble/curves': 1.4.2 '@noble/hashes': 1.4.0 '@scure/base': 1.1.9 + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@scure/bip39@1.3.0': dependencies: '@noble/hashes': 1.4.0 '@scure/base': 1.1.9 + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@sentry/core@8.32.0': dependencies: '@sentry/types': 8.32.0 @@ -8232,7 +8824,7 @@ snapshots: '@sentry/opentelemetry': 8.32.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) '@sentry/types': 8.32.0 '@sentry/utils': 8.32.0 - import-in-the-middle: 1.14.2 + import-in-the-middle: 1.15.0 transitivePeerDependencies: - supports-color @@ -8253,8 +8845,8 @@ snapshots: '@sentry/node': 8.32.0 '@sentry/types': 8.32.0 '@sentry/utils': 8.32.0 - detect-libc: 2.0.4 - node-abi: 3.75.0 + detect-libc: 2.1.2 + node-abi: 3.87.0 transitivePeerDependencies: - supports-color @@ -8272,7 +8864,7 @@ snapshots: '@sideway/pinpoint@2.0.0': {} - '@sinclair/typebox@0.27.8': {} + '@sinclair/typebox@0.27.10': {} '@sinonjs/commons@3.0.1': dependencies: @@ -8282,41 +8874,48 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + '@socket.io/component-emitter@3.1.2': {} - '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.5)': + '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.6)': dependencies: debug: 4.3.7 notepack.io: 3.0.1 - socket.io-adapter: 2.5.5 + socket.io-adapter: 2.5.6 uid2: 1.0.0 transitivePeerDependencies: - supports-color '@sqltools/formatter@1.2.5': {} + '@stablelib/base64@1.0.1': {} + '@testcontainers/postgresql@11.0.0': dependencies: - testcontainers: 11.5.1 + testcontainers: 11.11.0 transitivePeerDependencies: + - bare-abort-controller - bare-buffer + - react-native-b4a - supports-color - '@trivago/prettier-plugin-sort-imports@2.0.2(prettier@2.2.1)': + '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@2.2.1)': dependencies: - '@babel/core': 7.13.10 - '@babel/generator': 7.13.9 - '@babel/parser': 7.13.10 - '@babel/traverse': 7.13.0 - '@babel/types': 7.13.0 - '@types/lodash': 4.14.168 + '@babel/generator': 7.17.7 + '@babel/parser': 7.29.0 + '@babel/traverse': 7.23.2 + '@babel/types': 7.17.0 javascript-natural-sort: 0.7.1 - lodash: 4.17.21 + lodash: 4.17.23 prettier: 2.2.1 transitivePeerDependencies: - supports-color - '@tsconfig/node10@1.0.11': {} + '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -8326,24 +8925,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.0 - '@babel/types': 7.28.2 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.0 - '@babel/types': 7.28.2 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.2 + '@babel/types': 7.29.0 '@types/bn.js@5.1.6': dependencies: @@ -8383,7 +8982,7 @@ snapshots: '@types/node': 20.6.0 '@types/ssh2': 1.15.5 - '@types/dockerode@3.3.42': + '@types/dockerode@3.3.47': dependencies: '@types/docker-modem': 3.0.6 '@types/node': 20.6.0 @@ -8391,19 +8990,19 @@ snapshots: '@types/estree@1.0.8': {} - '@types/express-serve-static-core@4.19.6': + '@types/express-serve-static-core@4.19.8': dependencies: '@types/node': 20.6.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 - '@types/send': 0.17.5 + '@types/send': 1.2.1 '@types/express@4.17.11': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.6 + '@types/express-serve-static-core': 4.19.8 '@types/qs': 6.14.0 - '@types/serve-static': 1.15.8 + '@types/serve-static': 2.2.0 '@types/glob@7.2.0': dependencies: @@ -8440,23 +9039,20 @@ snapshots: '@types/json5@0.0.29': {} - '@types/jsonwebtoken@9.0.2': + '@types/jsonwebtoken@9.0.10': dependencies: + '@types/ms': 2.1.0 '@types/node': 20.6.0 - '@types/lodash@4.14.168': {} - '@types/methods@1.1.4': {} - '@types/mime@1.3.5': {} - '@types/minimatch@3.0.5': {} '@types/minimatch@5.1.2': {} '@types/minimatch@6.0.0': dependencies: - minimatch: 10.0.3 + minimatch: 3.1.4 '@types/minimist@1.2.5': {} @@ -8464,69 +9060,73 @@ snapshots: dependencies: '@types/node': 20.6.0 + '@types/ms@2.1.0': {} + '@types/mysql@2.15.26': dependencies: '@types/node': 20.6.0 '@types/node-cron@3.0.11': {} - '@types/node@18.19.122': + '@types/node@18.19.130': dependencies: undici-types: 5.26.5 '@types/node@20.6.0': {} + '@types/node@22.19.9': + dependencies: + undici-types: 6.21.0 + '@types/normalize-package-data@2.4.4': {} '@types/parse-json@4.0.2': {} '@types/pg-pool@2.0.6': dependencies: - '@types/pg': 8.15.5 + '@types/pg': 8.16.0 - '@types/pg@8.15.5': + '@types/pg@8.16.0': dependencies: '@types/node': 20.6.0 - pg-protocol: 1.10.3 + pg-protocol: 1.11.0 pg-types: 2.2.0 '@types/pg@8.6.1': dependencies: '@types/node': 20.6.0 - pg-protocol: 1.10.3 + pg-protocol: 1.11.0 pg-types: 2.2.0 '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} - '@types/semver@7.7.0': {} + '@types/semver@7.7.1': {} - '@types/send@0.17.5': + '@types/send@1.2.1': dependencies: - '@types/mime': 1.3.5 '@types/node': 20.6.0 - '@types/serve-static@1.15.8': + '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 '@types/node': 20.6.0 - '@types/send': 0.17.5 '@types/shimmer@1.2.0': {} - '@types/ssh2-streams@0.1.12': + '@types/ssh2-streams@0.1.13': dependencies: '@types/node': 20.6.0 '@types/ssh2@0.5.52': dependencies: '@types/node': 20.6.0 - '@types/ssh2-streams': 0.1.12 + '@types/ssh2-streams': 0.1.13 '@types/ssh2@1.15.5': dependencies: - '@types/node': 18.19.122 + '@types/node': 18.19.130 '@types/stack-utils@2.0.3': {} @@ -8539,13 +9139,19 @@ snapshots: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 '@types/node': 20.6.0 - form-data: 4.0.4 + form-data: 4.0.5 '@types/supertest@2.0.10': dependencies: '@types/superagent': 8.1.9 - '@types/validator@13.15.2': {} + '@types/triple-beam@1.3.5': {} + + '@types/uuid@11.0.0': + dependencies: + uuid: 13.0.0 + + '@types/validator@13.15.10': {} '@types/webidl-conversions@7.0.3': {} @@ -8555,24 +9161,24 @@ snapshots: '@types/yargs-parser@21.0.3': {} - '@types/yargs@17.0.33': + '@types/yargs@17.0.35': dependencies: '@types/yargs-parser': 21.0.3 '@typescript-eslint/eslint-plugin@6.5.0(@typescript-eslint/parser@6.5.0(eslint@7.22.0)(typescript@5.4.5))(eslint@7.22.0)(typescript@5.4.5)': dependencies: - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/regexpp': 4.12.2 '@typescript-eslint/parser': 6.5.0(eslint@7.22.0)(typescript@5.4.5) '@typescript-eslint/scope-manager': 6.5.0 '@typescript-eslint/type-utils': 6.5.0(eslint@7.22.0)(typescript@5.4.5) '@typescript-eslint/utils': 6.5.0(eslint@7.22.0)(typescript@5.4.5) '@typescript-eslint/visitor-keys': 6.5.0 - debug: 4.4.1 + debug: 4.4.3 eslint: 7.22.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - semver: 7.7.2 + semver: 7.7.4 ts-api-utils: 1.4.3(typescript@5.4.5) optionalDependencies: typescript: 5.4.5 @@ -8585,7 +9191,7 @@ snapshots: '@typescript-eslint/types': 6.5.0 '@typescript-eslint/typescript-estree': 6.5.0(typescript@5.4.5) '@typescript-eslint/visitor-keys': 6.5.0 - debug: 4.4.1 + debug: 4.4.3 eslint: 7.22.0 optionalDependencies: typescript: 5.4.5 @@ -8601,7 +9207,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.5.0(typescript@5.4.5) '@typescript-eslint/utils': 6.5.0(eslint@7.22.0)(typescript@5.4.5) - debug: 4.4.1 + debug: 4.4.3 eslint: 7.22.0 ts-api-utils: 1.4.3(typescript@5.4.5) optionalDependencies: @@ -8615,10 +9221,10 @@ snapshots: dependencies: '@typescript-eslint/types': 6.5.0 '@typescript-eslint/visitor-keys': 6.5.0 - debug: 4.4.1 + debug: 4.4.3 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.7.2 + semver: 7.7.4 ts-api-utils: 1.4.3(typescript@5.4.5) optionalDependencies: typescript: 5.4.5 @@ -8627,14 +9233,14 @@ snapshots: '@typescript-eslint/utils@6.5.0(eslint@7.22.0)(typescript@5.4.5)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@7.22.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@7.22.0) '@types/json-schema': 7.0.15 - '@types/semver': 7.7.0 + '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 6.5.0 '@typescript-eslint/types': 6.5.0 '@typescript-eslint/typescript-estree': 6.5.0(typescript@5.4.5) eslint: 7.22.0 - semver: 7.7.2 + semver: 7.7.4 transitivePeerDependencies: - supports-color - typescript @@ -8648,16 +9254,16 @@ snapshots: dependencies: '@vitest/spy': 3.0.9 '@vitest/utils': 3.0.9 - chai: 5.2.1 + chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.9(vite@6.3.5(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))': + '@vitest/mocker@3.0.9(vite@6.4.1(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.0.9 estree-walker: 3.0.3 - magic-string: 0.30.17 + magic-string: 0.30.21 optionalDependencies: - vite: 6.3.5(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1) + vite: 6.4.1(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@3.0.9': dependencies: @@ -8675,7 +9281,7 @@ snapshots: '@vitest/snapshot@3.0.9': dependencies: '@vitest/pretty-format': 3.0.9 - magic-string: 0.30.17 + magic-string: 0.30.21 pathe: 2.0.3 '@vitest/spy@3.0.9': @@ -8685,7 +9291,7 @@ snapshots: '@vitest/utils@3.0.9': dependencies: '@vitest/pretty-format': 3.0.9 - loupe: 3.2.0 + loupe: 3.2.1 tinyrainbow: 2.0.0 '@yarnpkg/lockfile@1.1.0': {} @@ -8695,6 +9301,10 @@ snapshots: jsonparse: 1.3.1 through: 2.3.8 + abitype@1.2.3(typescript@5.4.5): + optionalDependencies: + typescript: 5.4.5 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -8706,7 +9316,7 @@ snapshots: accepts@2.0.0: dependencies: - mime-types: 3.0.1 + mime-types: 3.0.2 negotiator: 1.0.0 acorn-import-attributes@1.9.5(acorn@8.15.0): @@ -8740,7 +9350,7 @@ snapshots: ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.0.6 + fast-uri: 3.1.0 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -8752,7 +9362,7 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} + ansi-regex@6.2.2: {} ansi-styles@3.2.1: dependencies: @@ -8764,9 +9374,9 @@ snapshots: ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} + ansi-styles@6.2.3: {} - any-promise@1.3.0: {} + ansis@4.2.0: {} anymatch@3.1.3: dependencies: @@ -8777,11 +9387,11 @@ snapshots: archiver-utils@5.0.2: dependencies: - glob: 10.3.15 + glob: 10.5.0 graceful-fs: 4.2.11 is-stream: 2.0.1 lazystream: 1.0.1 - lodash: 4.17.21 + lodash: 4.17.23 normalize-path: 3.0.0 readable-stream: 4.7.0 @@ -8794,6 +9404,9 @@ snapshots: readdir-glob: 1.1.3 tar-stream: 3.1.7 zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a arg@4.1.3: {} @@ -8803,14 +9416,10 @@ snapshots: arr-diff@4.0.0: {} - arr-flatten@1.1.0: {} - arr-union@3.1.0: {} array-differ@3.0.0: {} - array-find-index@1.0.2: {} - array-flatten@1.1.1: {} array-ify@1.0.0: {} @@ -8843,7 +9452,7 @@ snapshots: async@2.6.4: dependencies: - lodash: 4.17.21 + lodash: 4.17.23 async@3.2.6: {} @@ -8853,35 +9462,29 @@ snapshots: atob@2.1.2: {} + atomic-sleep@1.0.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 - axios@1.11.0: - dependencies: - follow-redirects: 1.15.11 - form-data: 4.0.4 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - - axios@1.5.1: + axios@1.13.5: dependencies: follow-redirects: 1.15.11 - form-data: 4.0.4 + form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - b4a@1.6.7: {} + b4a@1.7.3: {} - babel-jest@29.7.0(@babel/core@7.28.0): + babel-jest@29.7.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.28.0) + babel-preset-jest: 29.6.3(@babel/core@7.29.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -8890,7 +9493,7 @@ snapshots: babel-plugin-istanbul@6.1.1: dependencies: - '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-plugin-utils': 7.28.6 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-instrument: 5.2.1 @@ -8900,72 +9503,95 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.0): - dependencies: - '@babel/core': 7.28.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.0) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.0) - - babel-preset-jest@29.6.3(@babel/core@7.28.0): - dependencies: - '@babel/core': 7.28.0 + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + + babel-preset-jest@29.6.3(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.0) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) - bakosafe@0.2.1-beta.1(fuels@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1))): + bakosafe@0.6.0(fuels@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)))(typescript@5.4.5): dependencies: '@ethereumjs/util': 9.0.3 '@ethersproject/bytes': 5.7.0 - '@noble/curves': 1.8.1 - axios: 1.5.1 - fuels: 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@noble/curves': 1.9.7 + '@noble/secp256k1': 2.3.0 + axios: 1.13.5 + build: 0.1.4 + fuels: 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + lodash.partition: 4.6.0 + pnpm: 10.28.2 uuid: 9.0.1 + viem: 2.45.1(typescript@5.4.5) transitivePeerDependencies: + - bufferutil - debug + - typescript + - utf-8-validate + - zod balanced-match@1.0.2: {} - bare-events@2.6.1: - optional: true + balanced-match@4.0.4: {} + + bare-events@2.8.2: {} - bare-fs@4.2.0: + bare-fs@4.5.3: dependencies: - bare-events: 2.6.1 + bare-events: 2.8.2 bare-path: 3.0.0 - bare-stream: 2.6.5(bare-events@2.6.1) + bare-stream: 2.7.0(bare-events@2.8.2) + bare-url: 2.3.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a optional: true - bare-os@3.6.1: + bare-os@3.6.2: optional: true bare-path@3.0.0: dependencies: - bare-os: 3.6.1 + bare-os: 3.6.2 optional: true - bare-stream@2.6.5(bare-events@2.6.1): + bare-stream@2.7.0(bare-events@2.8.2): dependencies: - streamx: 2.22.1 + streamx: 2.23.0 optionalDependencies: - bare-events: 2.6.1 + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-url@2.3.2: + dependencies: + bare-path: 3.0.0 optional: true base64-js@1.5.1: {} @@ -8982,6 +9608,8 @@ snapshots: mixin-deep: 1.3.2 pascalcase: 0.1.1 + baseline-browser-mapping@2.9.19: {} + basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 @@ -9000,48 +9628,50 @@ snapshots: bn.js@5.2.1: {} - body-parser@1.19.0: + body-parser@1.20.3: dependencies: - bytes: 3.1.0 + bytes: 3.1.2 content-type: 1.0.5 debug: 2.6.9 - depd: 1.1.2 - http-errors: 1.7.2 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 iconv-lite: 0.4.24 - on-finished: 2.3.0 - qs: 6.7.0 - raw-body: 2.4.0 + on-finished: 2.4.1 + qs: 6.14.1 + raw-body: 2.5.2 type-is: 1.6.18 + unpipe: 1.0.0 transitivePeerDependencies: - supports-color - body-parser@1.20.2: + body-parser@1.20.4: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 2.6.9 depd: 2.0.0 destroy: 1.2.0 - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.11.0 - raw-body: 2.5.2 + qs: 6.14.1 + raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 transitivePeerDependencies: - supports-color - body-parser@2.2.0: + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1 - http-errors: 2.0.0 - iconv-lite: 0.6.3 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.0 + qs: 6.14.1 + raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: - supports-color @@ -9057,31 +9687,21 @@ snapshots: dependencies: balanced-match: 1.0.2 - braces@2.3.2: + brace-expansion@5.0.4: dependencies: - arr-flatten: 1.1.0 - array-unique: 0.3.2 - extend-shallow: 2.0.1 - fill-range: 4.0.0 - isobject: 3.0.1 - repeat-element: 1.1.4 - snapdragon: 0.8.2 - snapdragon-node: 2.1.1 - split-string: 3.1.0 - to-regex: 3.0.2 - transitivePeerDependencies: - - supports-color + balanced-match: 4.0.4 braces@3.0.3: dependencies: fill-range: 7.1.1 - browserslist@4.25.2: + browserslist@4.28.1: dependencies: - caniuse-lite: 1.0.30001734 - electron-to-chromium: 1.5.200 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.2) + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001768 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) bs-logger@0.2.6: dependencies: @@ -9111,17 +9731,30 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - buildcheck@0.0.6: + build@0.1.4: + dependencies: + cssmin: 0.3.2 + jsmin: 1.0.1 + jxLoader: 0.1.1 + moo-server: 1.3.0 + promised-io: 0.3.6 + timespan: 2.3.0 + uglify-js: 3.19.3 + walker: 1.0.8 + winston: 3.19.0 + wrench: 1.3.9 + + buildcheck@0.0.7: optional: true bull@4.16.5: dependencies: cron-parser: 4.9.0 get-port: 5.1.1 - ioredis: 5.7.0 - lodash: 4.17.21 - msgpackr: 1.11.5 - semver: 7.7.2 + ioredis: 5.9.2 + lodash: 4.17.23 + msgpackr: 1.11.8 + semver: 7.7.4 uuid: 8.3.2 transitivePeerDependencies: - supports-color @@ -9133,8 +9766,6 @@ snapshots: byline@5.0.0: {} - bytes@3.1.0: {} - bytes@3.1.2: {} cac@6.7.14: {} @@ -9172,31 +9803,24 @@ snapshots: callsites@3.1.0: {} - camelcase-keys@2.1.0: - dependencies: - camelcase: 2.1.1 - map-obj: 1.0.1 - camelcase-keys@6.2.2: dependencies: camelcase: 5.3.1 map-obj: 4.3.0 quick-lru: 4.0.1 - camelcase@2.1.1: {} - camelcase@5.3.1: {} camelcase@6.3.0: {} - caniuse-lite@1.0.30001734: {} + caniuse-lite@1.0.30001768: {} - chai@5.2.1: + chai@5.3.3: dependencies: assertion-error: 2.0.1 - check-error: 2.1.1 + check-error: 2.1.3 deep-eql: 5.0.2 - loupe: 3.2.0 + loupe: 3.2.1 pathval: 2.0.1 chalk@2.4.2: @@ -9217,7 +9841,7 @@ snapshots: char-regex@1.0.2: {} - check-error@2.1.1: {} + check-error@2.1.3: {} cheerio-select@2.1.0: dependencies: @@ -9265,9 +9889,9 @@ snapshots: class-validator@0.14.0: dependencies: - '@types/validator': 13.15.2 - libphonenumber-js: 1.12.10 - validator: 13.15.15 + '@types/validator': 13.15.10 + libphonenumber-js: 1.12.36 + validator: 13.15.26 clean-stack@2.2.0: {} @@ -9275,15 +9899,6 @@ snapshots: dependencies: restore-cursor: 3.1.0 - cli-highlight@2.1.11: - dependencies: - chalk: 4.1.2 - highlight.js: 10.7.3 - mz: 2.7.0 - parse5: 5.1.1 - parse5-htmlparser2-tree-adapter: 6.0.1 - yargs: 16.2.0 - cli-table@0.3.11: dependencies: colors: 1.0.3 @@ -9309,7 +9924,7 @@ snapshots: co@4.6.0: {} - collect-v8-coverage@1.0.2: {} + collect-v8-coverage@1.0.3: {} collection-visit@1.0.0: dependencies: @@ -9324,10 +9939,25 @@ snapshots: dependencies: color-name: 1.1.4 + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + color-name@1.1.3: {} color-name@1.1.4: {} + color-name@2.1.0: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + colorette@2.0.20: {} colors@1.0.3: {} @@ -9342,6 +9972,8 @@ snapshots: commander@6.2.1: {} + commander@9.5.0: {} + compare-func@2.0.0: dependencies: array-ify: 1.0.0 @@ -9362,21 +9994,19 @@ snapshots: concurrently@9.1.2: dependencies: chalk: 4.1.2 - lodash: 4.17.21 + lodash: 4.17.23 rxjs: 7.8.2 shell-quote: 1.8.3 supports-color: 8.1.1 tree-kill: 1.2.2 yargs: 17.7.2 - content-disposition@0.5.3: - dependencies: - safe-buffer: 5.1.2 - - content-disposition@1.0.0: + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.1: {} + content-type@1.0.5: {} conventional-changelog-angular@5.0.13: @@ -9387,20 +10017,18 @@ snapshots: conventional-changelog-conventionalcommits@4.6.3: dependencies: compare-func: 2.0.0 - lodash: 4.17.21 + lodash: 4.17.23 q: 1.5.1 conventional-commits-parser@3.2.4: dependencies: JSONStream: 1.3.5 is-text-path: 1.0.1 - lodash: 4.17.21 + lodash: 4.17.23 meow: 8.1.2 split2: 3.2.2 through2: 4.0.2 - convert-source-map@1.9.0: {} - convert-source-map@2.0.0: {} cookie-parser@1.4.6: @@ -9412,12 +10040,12 @@ snapshots: cookie-signature@1.2.2: {} - cookie@0.4.0: {} - cookie@0.4.1: {} cookie@0.4.2: {} + cookie@0.7.1: {} + cookie@0.7.2: {} cookiejar@2.1.4: {} @@ -9427,7 +10055,7 @@ snapshots: copyfiles@2.4.1: dependencies: glob: 7.2.3 - minimatch: 3.1.2 + minimatch: 3.1.4 mkdirp: 1.0.4 noms: 0.0.0 through2: 2.0.5 @@ -9451,8 +10079,8 @@ snapshots: cpu-features@0.0.10: dependencies: - buildcheck: 0.0.6 - nan: 2.23.0 + buildcheck: 0.0.7 + nan: 2.25.0 optional: true crc-32@1.2.2: {} @@ -9481,7 +10109,7 @@ snapshots: cron-parser@4.9.0: dependencies: - luxon: 3.7.1 + luxon: 3.7.2 cross-env@7.0.3: dependencies: @@ -9509,22 +10137,17 @@ snapshots: css-what@6.2.2: {} - currently-unhandled@0.4.1: - dependencies: - array-find-index: 1.0.2 + cssmin@0.3.2: {} dargs@7.0.0: {} date-fns@2.30.0: dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.28.6 - dateformat@1.0.12: - dependencies: - get-stdin: 4.0.1 - meow: 3.7.0 + dateformat@4.6.3: {} - dayjs@1.11.13: {} + dayjs@1.11.19: {} debug@2.6.9: dependencies: @@ -9538,7 +10161,7 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.1: + debug@4.4.3: dependencies: ms: 2.1.3 @@ -9553,7 +10176,7 @@ snapshots: dedent@0.7.0: {} - dedent@1.6.0: {} + dedent@1.7.1: {} deep-eql@5.0.2: {} @@ -9584,21 +10207,17 @@ snapshots: denque@2.1.0: {} - depd@1.1.2: {} - depd@2.0.0: {} - destroy@1.0.4: {} - destroy@1.2.0: {} - detect-libc@2.0.4: {} + detect-libc@2.1.2: {} detect-newline@3.1.0: {} diff-sequences@29.6.3: {} - diff@4.0.2: {} + diff@4.0.4: {} dir-glob@2.2.2: dependencies: @@ -9608,27 +10227,27 @@ snapshots: dependencies: path-type: 4.0.0 - docker-compose@1.2.0: + docker-compose@1.3.1: dependencies: - yaml: 2.8.1 + yaml: 2.8.2 docker-modem@5.0.6: dependencies: - debug: 4.4.1 + debug: 4.4.3 readable-stream: 3.6.2 split-ca: 1.0.1 - ssh2: 1.16.0 + ssh2: 1.17.0 transitivePeerDependencies: - supports-color - dockerode@4.0.7: + dockerode@4.0.9: dependencies: '@balena/dockerignore': 1.0.2 - '@grpc/grpc-js': 1.13.4 + '@grpc/grpc-js': 1.14.3 '@grpc/proto-loader': 0.7.15 docker-modem: 5.0.6 - protobufjs: 7.5.3 - tar-fs: 2.1.3 + protobufjs: 7.5.4 + tar-fs: 2.1.4 uuid: 10.0.0 transitivePeerDependencies: - supports-color @@ -9661,6 +10280,8 @@ snapshots: dotenv@16.4.5: {} + dotenv@16.6.1: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9683,7 +10304,7 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.200: {} + electron-to-chromium@1.5.286: {} emittery@0.13.1: {} @@ -9691,6 +10312,8 @@ snapshots: emoji-regex@9.2.2: {} + enabled@2.0.0: {} + encodeurl@1.0.2: {} encodeurl@2.0.0: {} @@ -9739,7 +10362,7 @@ snapshots: entities@6.0.1: {} - error-ex@1.3.2: + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -9760,6 +10383,37 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es6-promise@4.2.8: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.25.3: optionalDependencies: '@esbuild/aix-ppc64': 0.25.3 @@ -9788,34 +10442,34 @@ snapshots: '@esbuild/win32-ia32': 0.25.3 '@esbuild/win32-x64': 0.25.3 - esbuild@0.25.8: + esbuild@0.27.3: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.8 - '@esbuild/android-arm': 0.25.8 - '@esbuild/android-arm64': 0.25.8 - '@esbuild/android-x64': 0.25.8 - '@esbuild/darwin-arm64': 0.25.8 - '@esbuild/darwin-x64': 0.25.8 - '@esbuild/freebsd-arm64': 0.25.8 - '@esbuild/freebsd-x64': 0.25.8 - '@esbuild/linux-arm': 0.25.8 - '@esbuild/linux-arm64': 0.25.8 - '@esbuild/linux-ia32': 0.25.8 - '@esbuild/linux-loong64': 0.25.8 - '@esbuild/linux-mips64el': 0.25.8 - '@esbuild/linux-ppc64': 0.25.8 - '@esbuild/linux-riscv64': 0.25.8 - '@esbuild/linux-s390x': 0.25.8 - '@esbuild/linux-x64': 0.25.8 - '@esbuild/netbsd-arm64': 0.25.8 - '@esbuild/netbsd-x64': 0.25.8 - '@esbuild/openbsd-arm64': 0.25.8 - '@esbuild/openbsd-x64': 0.25.8 - '@esbuild/openharmony-arm64': 0.25.8 - '@esbuild/sunos-x64': 0.25.8 - '@esbuild/win32-arm64': 0.25.8 - '@esbuild/win32-ia32': 0.25.8 - '@esbuild/win32-x64': 0.25.8 + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 escalade@3.2.0: {} @@ -9833,7 +10487,7 @@ snapshots: dependencies: eslint: 7.22.0 prettier: 2.2.1 - prettier-linter-helpers: 1.0.0 + prettier-linter-helpers: 1.0.1 optionalDependencies: eslint-config-prettier: 8.1.0(eslint@7.22.0) @@ -9859,14 +10513,14 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.3 doctrine: 3.0.0 enquirer: 2.4.1 eslint-scope: 5.1.1 eslint-utils: 2.1.0 eslint-visitor-keys: 2.1.0 espree: 7.3.1 - esquery: 1.6.0 + esquery: 1.7.0 esutils: 2.0.3 file-entry-cache: 6.0.1 functional-red-black-tree: 1.0.1 @@ -9876,16 +10530,16 @@ snapshots: import-fresh: 3.3.1 imurmurhash: 0.1.4 is-glob: 4.0.3 - js-yaml: 3.14.1 + js-yaml: 3.14.2 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 - lodash: 4.17.21 - minimatch: 3.1.2 + lodash: 4.17.23 + minimatch: 3.1.4 natural-compare: 1.4.0 optionator: 0.9.4 progress: 2.0.3 regexpp: 3.2.0 - semver: 7.7.2 + semver: 7.5.2 strip-ansi: 6.0.1 strip-json-comments: 3.1.1 table: 6.9.0 @@ -9902,7 +10556,7 @@ snapshots: esprima@4.0.1: {} - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -9931,6 +10585,14 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@5.0.1: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + events@3.3.0: {} execa@4.1.0: @@ -9971,7 +10633,7 @@ snapshots: transitivePeerDependencies: - supports-color - expect-type@1.2.2: {} + expect-type@1.3.0: {} expect@29.7.0: dependencies: @@ -9989,67 +10651,69 @@ snapshots: dependencies: joi: 17.4.0 - express@4.17.1: + express@4.21.2: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.19.0 - content-disposition: 0.5.3 + body-parser: 1.20.3 + content-disposition: 0.5.4 content-type: 1.0.5 - cookie: 0.4.0 + cookie: 0.7.1 cookie-signature: 1.0.6 debug: 2.6.9 - depd: 1.1.2 - encodeurl: 1.0.2 + depd: 2.0.0 + encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.1.2 + finalhandler: 1.3.1 fresh: 0.5.2 - merge-descriptors: 1.0.1 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 methods: 1.1.2 - on-finished: 2.3.0 + on-finished: 2.4.1 parseurl: 1.3.3 - path-to-regexp: 0.1.7 + path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.7.0 + qs: 6.14.1 range-parser: 1.2.1 - safe-buffer: 5.1.2 - send: 0.17.1 - serve-static: 1.14.1 - setprototypeof: 1.1.1 - statuses: 1.5.0 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 type-is: 1.6.18 utils-merge: 1.0.1 vary: 1.1.2 transitivePeerDependencies: - supports-color - express@5.1.0: + express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.0 - content-disposition: 1.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.1 + debug: 4.4.3 + depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.0 + finalhandler: 2.1.1 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 2.0.0 - mime-types: 3.0.1 + mime-types: 3.0.2 on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.14.1 range-parser: 1.2.1 router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 @@ -10078,6 +10742,8 @@ snapshots: transitivePeerDependencies: - supports-color + fast-copy@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -10107,11 +10773,15 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-redact@3.5.0: {} + fast-safe-stringify@2.1.1: {} - fast-uri@3.0.6: {} + fast-sha256@1.3.0: {} + + fast-uri@3.1.0: {} - fastq@1.19.1: + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -10119,10 +10789,12 @@ snapshots: dependencies: bser: 2.1.1 - fdir@6.4.6(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + fecha@4.2.3: {} + fflate@0.8.2: {} file-entry-cache@6.0.1: @@ -10131,34 +10803,27 @@ snapshots: filelist@1.0.4: dependencies: - minimatch: 5.1.6 - - fill-range@4.0.0: - dependencies: - extend-shallow: 2.0.1 - is-number: 3.0.0 - repeat-string: 1.6.1 - to-regex-range: 2.1.1 + minimatch: 5.1.8 fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - finalhandler@1.1.2: + finalhandler@1.3.1: dependencies: debug: 2.6.9 - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 - on-finished: 2.3.0 + on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 1.5.0 + statuses: 2.0.1 unpipe: 1.0.0 transitivePeerDependencies: - supports-color - finalhandler@2.1.0: + finalhandler@2.1.1: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -10167,11 +10832,6 @@ snapshots: transitivePeerDependencies: - supports-color - find-up@1.1.2: - dependencies: - path-exists: 2.1.0 - pinkie-promise: 2.0.1 - find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -10194,6 +10854,8 @@ snapshots: flatted@3.3.3: {} + fn.name@1.1.0: {} + follow-redirects@1.15.11: {} for-each@0.3.5: @@ -10215,7 +10877,7 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - form-data@4.0.4: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -10243,7 +10905,7 @@ snapshots: dependencies: at-least-node: 1.0.0 graceful-fs: 4.2.11 - jsonfile: 6.1.0 + jsonfile: 6.2.0 universalify: 2.0.1 fs.realpath@1.0.0: {} @@ -10251,22 +10913,22 @@ snapshots: fsevents@2.3.3: optional: true - fuels@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)): + fuels@0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: - '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/abi-typegen': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/account': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/address': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/contract': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/abi-coder': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/abi-typegen': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/account': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/address': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/contract': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/crypto': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/errors': 0.101.3 - '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/hasher': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/math': 0.101.3 - '@fuel-ts/program': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/recipes': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/script': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) - '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@fuel-ts/program': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/recipes': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/script': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/transactions': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) + '@fuel-ts/utils': 0.101.3(vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@fuel-ts/versions': 0.101.3 '@fuels/vm-asm': 0.60.2 bundle-require: 5.1.0(esbuild@0.25.3) @@ -10274,7 +10936,7 @@ snapshots: chokidar: 3.6.0 commander: 13.1.0 esbuild: 0.25.3 - glob: 10.4.5 + glob: 10.5.0 handlebars: 4.7.8 joycon: 3.1.1 lodash.camelcase: 4.3.0 @@ -10323,8 +10985,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stdin@4.0.1: {} - get-stdin@8.0.0: {} get-stream@5.2.0: @@ -10333,7 +10993,11 @@ snapshots: get-stream@6.0.1: {} - get-tsconfig@4.10.1: + get-tsconfig@4.13.3: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-tsconfig@4.13.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -10342,7 +11006,7 @@ snapshots: git-raw-commits@2.0.11: dependencies: dargs: 7.0.0 - lodash: 4.17.21 + lodash: 4.17.23 meow: 8.1.2 split2: 3.2.2 through2: 4.0.2 @@ -10358,19 +11022,11 @@ snapshots: glob-to-regexp@0.3.0: {} - glob@10.3.15: - dependencies: - foreground-child: 3.3.1 - jackspeak: 2.3.6 - minimatch: 9.0.5 - minipass: 7.1.2 - path-scurry: 1.11.1 - - glob@10.4.5: + glob@10.5.0: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 - minimatch: 9.0.5 + minimatch: 9.0.7 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 @@ -10380,7 +11036,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 3.1.4 once: 1.4.0 path-is-absolute: 1.0.1 @@ -10485,7 +11141,7 @@ snapshots: dependencies: function-bind: 1.1.2 - highlight.js@10.7.3: {} + help-me@5.0.0: {} hosted-git-info@2.8.9: {} @@ -10502,28 +11158,20 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 - http-errors@1.7.2: - dependencies: - depd: 1.1.2 - inherits: 2.0.3 - setprototypeof: 1.1.1 - statuses: 1.5.0 - toidentifier: 1.0.0 - - http-errors@1.7.3: + http-errors@2.0.0: dependencies: - depd: 1.1.2 + depd: 2.0.0 inherits: 2.0.4 - setprototypeof: 1.1.1 - statuses: 1.5.0 - toidentifier: 1.0.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 - http-errors@2.0.0: + http-errors@2.0.1: dependencies: depd: 2.0.0 inherits: 2.0.4 setprototypeof: 1.2.0 - statuses: 2.0.1 + statuses: 2.0.2 toidentifier: 1.0.1 human-signals@1.1.1: {} @@ -10536,7 +11184,7 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.6.3: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -10551,7 +11199,7 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-in-the-middle@1.14.2: + import-in-the-middle@1.15.0: dependencies: acorn: 8.15.0 acorn-import-attributes: 1.9.5(acorn@8.15.0) @@ -10565,10 +11213,6 @@ snapshots: imurmurhash@0.1.4: {} - indent-string@2.1.0: - dependencies: - repeating: 2.0.1 - indent-string@4.0.0: {} inflight@1.0.6: @@ -10576,17 +11220,15 @@ snapshots: once: 1.4.0 wrappy: 1.0.2 - inherits@2.0.3: {} - inherits@2.0.4: {} ini@1.3.8: {} - ioredis@5.7.0: + ioredis@5.9.2: dependencies: - '@ioredis/commands': 1.3.0 + '@ioredis/commands': 1.5.0 cluster-key-slot: 1.1.2 - debug: 4.4.1 + debug: 4.4.3 denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -10640,8 +11282,6 @@ snapshots: is-extglob@2.1.1: {} - is-finite@1.1.0: {} - is-fullwidth-code-point@3.0.0: {} is-generator-fn@2.1.0: {} @@ -10682,12 +11322,10 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.19 + which-typed-array: 1.1.20 is-unicode-supported@0.1.0: {} - is-utf8@0.2.1: {} - is-windows@1.0.2: {} is-wsl@2.2.0: @@ -10708,12 +11346,16 @@ snapshots: isobject@3.0.1: {} + isows@1.0.7(ws@8.18.3): + dependencies: + ws: 8.18.3 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.28.0 - '@babel/parser': 7.28.0 + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -10722,11 +11364,11 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.28.0 - '@babel/parser': 7.28.0 + '@babel/core': 7.29.0 + '@babel/parser': 7.29.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.2 + semver: 7.7.4 transitivePeerDependencies: - supports-color @@ -10738,23 +11380,17 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.1 + debug: 4.4.3 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: - supports-color - istanbul-reports@3.1.7: + istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jackspeak@2.3.6: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -10784,7 +11420,7 @@ snapshots: '@types/node': 20.6.0 chalk: 4.1.2 co: 4.6.0 - dedent: 1.6.0 + dedent: 1.7.1 is-generator-fn: 2.1.0 jest-each: 29.7.0 jest-matcher-utils: 29.7.0 @@ -10822,10 +11458,10 @@ snapshots: jest-config@29.7.0(@types/node@20.6.0)(ts-node@10.9.2(@types/node@20.6.0)(typescript@5.4.5)): dependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.0) + babel-jest: 29.7.0(@babel/core@7.29.0) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 @@ -10911,7 +11547,7 @@ snapshots: jest-message-util@29.7.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@jest/types': 29.6.3 '@types/stack-utils': 2.0.3 chalk: 4.1.2 @@ -10948,7 +11584,7 @@ snapshots: jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) jest-util: 29.7.0 jest-validate: 29.7.0 - resolve: 1.22.10 + resolve: 1.22.11 resolve.exports: 2.0.3 slash: 3.0.0 @@ -10990,7 +11626,7 @@ snapshots: '@types/node': 20.6.0 chalk: 4.1.2 cjs-module-lexer: 1.4.3 - collect-v8-coverage: 1.0.2 + collect-v8-coverage: 1.0.3 glob: 7.2.3 graceful-fs: 4.2.11 jest-haste-map: 29.7.0 @@ -11007,15 +11643,15 @@ snapshots: jest-snapshot@29.7.0: dependencies: - '@babel/core': 7.28.0 - '@babel/generator': 7.28.0 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) - '@babel/types': 7.28.2 + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.0) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -11026,7 +11662,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.7.2 + semver: 7.7.4 transitivePeerDependencies: - supports-color @@ -11103,10 +11739,17 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + jsesc@2.5.2: {} jsesc@3.1.0: {} + jsmin@1.0.1: {} + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -11131,7 +11774,7 @@ snapshots: json5@2.2.3: {} - jsonfile@6.1.0: + jsonfile@6.2.0: dependencies: universalify: 2.0.1 optionalDependencies: @@ -11141,24 +11784,37 @@ snapshots: jsonparse@1.3.1: {} - jsonwebtoken@9.0.1: + jsonwebtoken@9.0.3: dependencies: - jws: 3.2.2 - lodash: 4.17.21 + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.7.2 + semver: 7.7.4 - jwa@1.4.2: + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - jws@3.2.2: + jws@4.0.1: dependencies: - jwa: 1.4.2 + jwa: 2.0.1 safe-buffer: 5.2.1 + jxLoader@0.1.1: + dependencies: + js-yaml: 3.14.1 + moo-server: 1.3.0 + promised-io: 0.3.6 + walker: 1.0.8 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -11179,6 +11835,8 @@ snapshots: kleur@3.0.3: {} + kuler@2.0.0: {} + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 @@ -11190,7 +11848,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.12.10: {} + libphonenumber-js@1.12.36: {} lines-and-columns@1.2.4: {} @@ -11200,7 +11858,7 @@ snapshots: cli-truncate: 2.1.0 commander: 6.2.1 cosmiconfig: 7.1.0 - debug: 4.4.1 + debug: 4.4.3 dedent: 0.7.0 enquirer: 2.4.1 execa: 4.1.0 @@ -11227,14 +11885,6 @@ snapshots: optionalDependencies: enquirer: 2.4.1 - load-json-file@1.1.0: - dependencies: - graceful-fs: 4.2.11 - parse-json: 2.2.0 - pify: 2.3.0 - pinkie-promise: 2.0.1 - strip-bom: 2.0.0 - load-tsconfig@0.2.5: {} locate-path@5.0.0: @@ -11249,13 +11899,29 @@ snapshots: lodash.defaults@4.2.0: {} + lodash.includes@4.3.0: {} + lodash.isarguments@3.1.0: {} + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.memoize@4.1.2: {} + lodash.once@4.1.1: {} + + lodash.partition@4.6.0: {} + lodash.truncate@4.4.2: {} - lodash@4.17.21: {} + lodash@4.17.23: {} log-symbols@4.1.0: dependencies: @@ -11269,14 +11935,18 @@ snapshots: slice-ansi: 4.0.0 wrap-ansi: 6.2.0 - long@5.3.2: {} - - loud-rejection@1.6.0: + logform@2.7.0: dependencies: - currently-unhandled: 0.4.1 - signal-exit: 3.0.7 + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + long@5.3.2: {} - loupe@3.2.0: {} + loupe@3.2.1: {} lru-cache@10.4.3: {} @@ -11288,15 +11958,15 @@ snapshots: dependencies: yallist: 4.0.0 - luxon@3.7.1: {} + luxon@3.7.2: {} - magic-string@0.30.17: + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 make-dir@4.0.0: dependencies: - semver: 7.7.2 + semver: 7.7.4 make-error@1.3.6: {} @@ -11322,19 +11992,6 @@ snapshots: memory-pager@1.5.0: {} - meow@3.7.0: - dependencies: - camelcase-keys: 2.1.0 - decamelize: 1.2.0 - loud-rejection: 1.6.0 - map-obj: 1.0.1 - minimist: 1.2.8 - normalize-package-data: 2.5.0 - object-assign: 4.1.1 - read-pkg-up: 1.0.1 - redent: 1.0.0 - trim-newlines: 1.0.0 - meow@8.1.2: dependencies: '@types/minimist': 1.2.5 @@ -11349,7 +12006,7 @@ snapshots: type-fest: 0.18.1 yargs-parser: 20.2.9 - merge-descriptors@1.0.1: {} + merge-descriptors@1.0.3: {} merge-descriptors@2.0.0: {} @@ -11363,7 +12020,7 @@ snapshots: dependencies: arr-diff: 4.0.0 array-unique: 0.3.2 - braces: 2.3.2 + braces: 3.0.3 define-property: 2.0.2 extend-shallow: 3.0.2 extglob: 2.0.4 @@ -11390,7 +12047,7 @@ snapshots: dependencies: mime-db: 1.52.0 - mime-types@3.0.1: + mime-types@3.0.2: dependencies: mime-db: 1.54.0 @@ -11402,21 +12059,17 @@ snapshots: min-indent@1.0.1: {} - minimatch@10.0.3: - dependencies: - '@isaacs/brace-expansion': 5.0.0 - - minimatch@3.1.2: + minimatch@3.1.4: dependencies: brace-expansion: 1.1.12 - minimatch@5.1.6: + minimatch@5.1.8: dependencies: brace-expansion: 2.0.2 - minimatch@9.0.5: + minimatch@9.0.7: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 5.0.4 minimist-options@4.1.0: dependencies: @@ -11441,8 +12094,6 @@ snapshots: mkdirp@1.0.4: {} - mkdirp@2.1.6: {} - mkdirp@3.0.1: {} module-details-from-path@1.0.4: {} @@ -11452,12 +12103,14 @@ snapshots: '@types/whatwg-url': 11.0.5 whatwg-url: 14.2.0 - mongodb@6.18.0: + mongodb@6.21.0: dependencies: - '@mongodb-js/saslprep': 1.3.0 + '@mongodb-js/saslprep': 1.4.5 bson: 6.10.4 mongodb-connection-string-url: 3.0.2 + moo-server@1.3.0: {} + morgan@1.10.0: dependencies: basic-auth: 2.0.1 @@ -11472,8 +12125,6 @@ snapshots: ms@2.0.0: {} - ms@2.1.1: {} - ms@2.1.3: {} msgpackr-extract@3.0.3: @@ -11488,7 +12139,7 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 optional: true - msgpackr@1.11.5: + msgpackr@1.11.8: optionalDependencies: msgpackr-extract: 3.0.3 @@ -11498,15 +12149,11 @@ snapshots: array-differ: 3.0.0 array-union: 2.1.0 arrify: 2.0.1 - minimatch: 3.1.2 + minimatch: 3.1.4 - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 + mylas@2.1.14: {} - nan@2.23.0: + nan@2.25.0: optional: true nanoid@3.3.11: {} @@ -11535,9 +12182,9 @@ snapshots: neo-async@2.6.2: {} - node-abi@3.75.0: + node-abi@3.87.0: dependencies: - semver: 7.7.2 + semver: 7.5.2 node-cron@3.0.3: dependencies: @@ -11549,14 +12196,14 @@ snapshots: node-gyp-build-optional-packages@5.2.2: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 optional: true node-int64@0.4.0: {} - node-releases@2.0.19: {} + node-releases@2.0.27: {} - nodemailer@6.9.8: {} + nodemailer@8.0.1: {} noms@0.0.0: dependencies: @@ -11566,7 +12213,7 @@ snapshots: normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.10 + resolve: 1.22.11 semver: 5.7.2 validate-npm-package-license: 3.0.4 @@ -11574,7 +12221,7 @@ snapshots: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.16.1 - semver: 7.7.2 + semver: 7.5.2 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -11585,7 +12232,7 @@ snapshots: dependencies: path-key: 3.1.1 - npm@10.9.3: {} + npm@10.9.4: {} nth-check@2.1.1: dependencies: @@ -11611,6 +12258,8 @@ snapshots: dependencies: isobject: 3.0.1 + on-exit-leak-free@2.1.2: {} + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -11625,6 +12274,10 @@ snapshots: dependencies: wrappy: 1.0.2 + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -11645,6 +12298,21 @@ snapshots: os-tmpdir@1.0.2: {} + ox@0.11.3(typescript@5.4.5): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.4.5) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - zod + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -11675,30 +12343,18 @@ snapshots: dependencies: callsites: 3.1.0 - parse-json@2.2.0: - dependencies: - error-ex: 1.3.2 - parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 - error-ex: 1.3.2 + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse5-htmlparser2-tree-adapter@6.0.1: - dependencies: - parse5: 6.0.1 - parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 parse5: 7.3.0 - parse5@5.1.1: {} - - parse5@6.0.1: {} - parse5@7.3.0: dependencies: entities: 6.0.1 @@ -11720,17 +12376,13 @@ snapshots: minimist: 1.2.8 open: 7.4.2 rimraf: 2.7.1 - semver: 7.7.2 + semver: 7.7.4 slash: 2.0.0 tmp: 0.0.33 - yaml: 2.8.1 + yaml: 2.8.2 path-dirname@1.0.2: {} - path-exists@2.1.0: - dependencies: - pinkie-promise: 2.0.1 - path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -11744,15 +12396,9 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@0.1.7: {} - - path-to-regexp@8.2.0: {} + path-to-regexp@0.1.12: {} - path-type@1.1.0: - dependencies: - graceful-fs: 4.2.11 - pify: 2.3.0 - pinkie-promise: 2.0.1 + path-to-regexp@8.3.0: {} path-type@3.0.0: dependencies: @@ -11764,21 +12410,21 @@ snapshots: pathval@2.0.1: {} - pg-connection-string@2.9.1: {} + pg-connection-string@2.11.0: {} pg-int8@1.0.1: {} - pg-pool@3.10.1(pg@8.5.1): + pg-pool@3.11.0(pg@8.5.1): dependencies: pg: 8.5.1 - pg-protocol@1.10.3: {} + pg-protocol@1.11.0: {} pg-types@2.2.0: dependencies: pg-int8: 1.0.1 postgres-array: 2.0.0 - postgres-bytea: 1.0.0 + postgres-bytea: 1.0.1 postgres-date: 1.0.7 postgres-interval: 1.2.0 @@ -11786,9 +12432,9 @@ snapshots: dependencies: buffer-writer: 2.0.0 packet-reader: 1.0.0 - pg-connection-string: 2.9.1 - pg-pool: 3.10.1(pg@8.5.1) - pg-protocol: 1.10.3 + pg-connection-string: 2.11.0 + pg-pool: 3.11.0(pg@8.5.1) + pg-protocol: 1.11.0 pg-types: 2.2.0 pgpass: 1.0.5 @@ -11802,17 +12448,51 @@ snapshots: picomatch@4.0.3: {} - pify@2.3.0: {} - pify@3.0.0: {} pify@4.0.1: {} - pinkie-promise@2.0.1: + pino-abstract-transport@1.2.0: + dependencies: + readable-stream: 4.7.0 + split2: 4.2.0 + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@11.2.2: dependencies: - pinkie: 2.0.4 + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 1.2.0 + pump: 3.0.3 + readable-stream: 4.7.0 + secure-json-parse: 2.7.0 + sonic-boom: 4.2.0 + strip-json-comments: 3.1.1 + + pino-std-serializers@7.1.0: {} - pinkie@2.0.4: {} + pino@9.6.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 4.0.1 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 pirates@4.0.7: {} @@ -11824,6 +12504,12 @@ snapshots: dependencies: semver-compare: 1.0.0 + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + + pnpm@10.28.2: {} + portfinder@1.0.32: dependencies: async: 2.6.4 @@ -11844,7 +12530,7 @@ snapshots: postgres-array@2.0.0: {} - postgres-bytea@1.0.0: {} + postgres-bytea@1.0.1: {} postgres-date@1.0.7: {} @@ -11854,7 +12540,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.0: + prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 @@ -11878,10 +12564,14 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@4.0.1: {} + process@0.11.10: {} progress@2.0.3: {} + promised-io@0.3.6: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -11899,7 +12589,22 @@ snapshots: property-expr@2.0.6: {} - protobufjs@7.5.3: + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.6.0 + long: 5.3.2 + + protobufjs@8.0.0: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 @@ -11932,68 +12637,53 @@ snapshots: q@1.5.1: {} - qs@6.11.0: - dependencies: - side-channel: 1.1.0 - - qs@6.12.1: + qs@6.14.1: dependencies: side-channel: 1.1.0 - qs@6.14.0: - dependencies: - side-channel: 1.1.0 + querystringify@2.2.0: {} - qs@6.7.0: {} + queue-lit@1.5.2: {} queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + quick-lru@4.0.1: {} ramda@0.30.1: {} range-parser@1.2.1: {} - raw-body@2.4.0: + raw-body@2.5.2: dependencies: - bytes: 3.1.0 - http-errors: 1.7.2 + bytes: 3.1.2 + http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 - raw-body@2.5.2: + raw-body@2.5.3: dependencies: bytes: 3.1.2 - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.4.24 unpipe: 1.0.0 - raw-body@3.0.0: + raw-body@3.0.2: dependencies: bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 unpipe: 1.0.0 react-is@18.3.1: {} - read-pkg-up@1.0.1: - dependencies: - find-up: 1.1.2 - read-pkg: 1.1.0 - read-pkg-up@7.0.1: dependencies: find-up: 4.1.0 read-pkg: 5.2.0 type-fest: 0.8.1 - read-pkg@1.1.0: - dependencies: - load-json-file: 1.1.0 - normalize-package-data: 2.5.0 - path-type: 1.1.0 - read-pkg@5.2.0: dependencies: '@types/normalize-package-data': 2.4.4 @@ -12034,16 +12724,13 @@ snapshots: readdir-glob@1.1.3: dependencies: - minimatch: 5.1.6 + minimatch: 5.1.8 readdirp@3.6.0: dependencies: picomatch: 2.3.1 - redent@1.0.0: - dependencies: - indent-string: 2.1.0 - strip-indent: 1.0.1 + real-require@0.2.0: {} redent@3.0.0: dependencies: @@ -12054,7 +12741,7 @@ snapshots: redis-info@3.1.0: dependencies: - lodash: 4.17.21 + lodash: 4.17.23 redis-parser@3.0.0: dependencies: @@ -12080,26 +12767,20 @@ snapshots: regexpp@3.2.0: {} - repeat-element@1.1.4: {} - - repeat-string@1.6.1: {} - - repeating@2.0.1: - dependencies: - is-finite: 1.1.0 - require-directory@2.1.1: {} require-from-string@2.0.2: {} require-in-the-middle@7.5.2: dependencies: - debug: 4.4.1 + debug: 4.4.3 module-details-from-path: 1.0.4 - resolve: 1.22.10 + resolve: 1.22.11 transitivePeerDependencies: - supports-color + requires-port@1.0.0: {} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -12118,7 +12799,7 @@ snapshots: resolve.exports@2.0.3: {} - resolve@1.22.10: + resolve@1.22.11: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 @@ -12147,41 +12828,46 @@ snapshots: rimraf@5.0.10: dependencies: - glob: 10.3.15 + glob: 10.5.0 - rollup@4.46.2: + rollup@4.59.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.46.2 - '@rollup/rollup-android-arm64': 4.46.2 - '@rollup/rollup-darwin-arm64': 4.46.2 - '@rollup/rollup-darwin-x64': 4.46.2 - '@rollup/rollup-freebsd-arm64': 4.46.2 - '@rollup/rollup-freebsd-x64': 4.46.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 - '@rollup/rollup-linux-arm-musleabihf': 4.46.2 - '@rollup/rollup-linux-arm64-gnu': 4.46.2 - '@rollup/rollup-linux-arm64-musl': 4.46.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 - '@rollup/rollup-linux-ppc64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-musl': 4.46.2 - '@rollup/rollup-linux-s390x-gnu': 4.46.2 - '@rollup/rollup-linux-x64-gnu': 4.46.2 - '@rollup/rollup-linux-x64-musl': 4.46.2 - '@rollup/rollup-win32-arm64-msvc': 4.46.2 - '@rollup/rollup-win32-ia32-msvc': 4.46.2 - '@rollup/rollup-win32-x64-msvc': 4.46.2 + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 router@2.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 - path-to-regexp: 8.2.0 + path-to-regexp: 8.3.0 transitivePeerDependencies: - supports-color @@ -12201,47 +12887,51 @@ snapshots: dependencies: ret: 0.1.15 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} + secure-json-parse@2.7.0: {} + semver-compare@1.0.0: {} semver@5.7.2: {} semver@6.3.1: {} - semver@7.3.5: + semver@7.5.2: dependencies: lru-cache: 6.0.0 - semver@7.7.2: {} + semver@7.7.4: {} - send@0.17.1: + send@0.19.0: dependencies: debug: 2.6.9 - depd: 1.1.2 - destroy: 1.0.4 + depd: 2.0.0 + destroy: 1.2.0 encodeurl: 1.0.2 escape-html: 1.0.3 etag: 1.8.1 fresh: 0.5.2 - http-errors: 1.7.3 + http-errors: 2.0.0 mime: 1.6.0 - ms: 2.1.1 - on-finished: 2.3.0 + ms: 2.1.3 + on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 1.5.0 + statuses: 2.0.1 transitivePeerDependencies: - supports-color - send@1.2.0: + send@1.2.1: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 fresh: 2.0.0 - http-errors: 2.0.0 - mime-types: 3.0.1 + http-errors: 2.0.1 + mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 @@ -12249,21 +12939,21 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@1.14.1: + serve-static@1.16.2: dependencies: - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.17.1 + send: 0.19.0 transitivePeerDependencies: - supports-color - serve-static@2.2.0: + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.0 + send: 1.2.1 transitivePeerDependencies: - supports-color @@ -12283,15 +12973,13 @@ snapshots: is-plain-object: 2.0.4 split-string: 3.1.0 - setprototypeof@1.1.1: {} - setprototypeof@1.2.0: {} sha.js@2.4.12: dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 - to-buffer: 1.2.1 + to-buffer: 1.2.2 shebang-command@2.0.0: dependencies: @@ -12355,16 +13043,6 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - snapdragon-node@2.1.1: - dependencies: - define-property: 1.0.0 - isobject: 3.0.1 - snapdragon-util: 3.0.1 - - snapdragon-util@3.0.1: - dependencies: - kind-of: 3.2.2 - snapdragon@0.8.2: dependencies: base: 0.11.2 @@ -12378,10 +13056,10 @@ snapshots: transitivePeerDependencies: - supports-color - socket.io-adapter@2.5.5: + socket.io-adapter@2.5.6: dependencies: - debug: 4.3.7 - ws: 8.17.1 + debug: 4.4.3 + ws: 8.18.3 transitivePeerDependencies: - bufferutil - supports-color @@ -12392,16 +13070,16 @@ snapshots: '@socket.io/component-emitter': 3.1.2 debug: 4.3.7 engine.io-client: 6.5.4 - socket.io-parser: 4.2.4 + socket.io-parser: 4.2.5 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - socket.io-parser@4.2.4: + socket.io-parser@4.2.5: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -12412,13 +13090,17 @@ snapshots: cors: 2.8.5 debug: 4.3.7 engine.io: 6.5.5 - socket.io-adapter: 2.5.5 - socket.io-parser: 4.2.4 + socket.io-adapter: 2.5.6 + socket.io-parser: 4.2.5 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map-resolve@0.5.3: @@ -12477,18 +13159,22 @@ snapshots: sprintf-js@1.0.3: {} + sql-highlight@6.1.0: {} + ssh-remote-port-forward@1.0.4: dependencies: '@types/ssh2': 0.5.52 - ssh2: 1.16.0 + ssh2: 1.17.0 - ssh2@1.16.0: + ssh2@1.17.0: dependencies: asn1: 0.2.6 bcrypt-pbkdf: 1.0.2 optionalDependencies: cpu-features: 0.0.10 - nan: 2.23.0 + nan: 2.25.0 + + stack-trace@0.0.10: {} stack-utils@2.0.6: dependencies: @@ -12503,20 +13189,20 @@ snapshots: define-property: 0.2.5 object-copy: 0.1.0 - statuses@1.5.0: {} - statuses@2.0.1: {} statuses@2.0.2: {} - std-env@3.9.0: {} + std-env@3.10.0: {} - streamx@2.22.1: + streamx@2.23.0: dependencies: + events-universal: 1.0.1 fast-fifo: 1.3.2 text-decoder: 1.2.3 - optionalDependencies: - bare-events: 2.6.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a string-argv@0.3.1: {} @@ -12535,7 +13221,7 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string_decoder@0.10.31: {} @@ -12557,13 +13243,9 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.1.0 - - strip-bom@2.0.0: + strip-ansi@7.1.2: dependencies: - is-utf8: 0.2.1 + ansi-regex: 6.2.2 strip-bom@3.0.0: {} @@ -12571,10 +13253,6 @@ snapshots: strip-final-newline@2.0.0: {} - strip-indent@1.0.1: - dependencies: - get-stdin: 4.0.1 - strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -12587,15 +13265,15 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.1 + debug: 4.4.3 fast-safe-stringify: 2.1.1 form-data: 3.0.4 formidable: 1.2.6 methods: 1.1.2 mime: 2.6.0 - qs: 6.12.1 + qs: 6.14.1 readable-stream: 3.6.2 - semver: 7.7.2 + semver: 7.5.2 transitivePeerDependencies: - supports-color @@ -12620,6 +13298,15 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svix@1.76.1: + dependencies: + '@stablelib/base64': 1.0.1 + '@types/node': 22.19.9 + es6-promise: 4.2.8 + fast-sha256: 1.3.0 + url-parse: 1.5.10 + uuid: 10.0.0 + table@6.9.0: dependencies: ajv: 8.17.1 @@ -12628,22 +13315,24 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tar-fs@2.1.3: + tar-fs@2.1.4: dependencies: chownr: 1.1.4 mkdirp-classic: 0.5.3 pump: 3.0.3 tar-stream: 2.2.0 - tar-fs@3.1.0: + tar-fs@3.1.1: dependencies: pump: 3.0.3 tar-stream: 3.1.7 optionalDependencies: - bare-fs: 4.2.0 + bare-fs: 4.5.3 bare-path: 3.0.0 transitivePeerDependencies: + - bare-abort-controller - bare-buffer + - react-native-b4a tar-stream@2.2.0: dependencies: @@ -12655,52 +13344,57 @@ snapshots: tar-stream@3.1.7: dependencies: - b4a: 1.6.7 + b4a: 1.7.3 fast-fifo: 1.3.2 - streamx: 2.22.1 + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a test-exclude@6.0.0: dependencies: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 - minimatch: 3.1.2 + minimatch: 3.1.4 - testcontainers@11.5.1: + testcontainers@11.11.0: dependencies: '@balena/dockerignore': 1.0.2 - '@types/dockerode': 3.3.42 + '@types/dockerode': 3.3.47 archiver: 7.0.1 async-lock: 1.4.1 byline: 5.0.0 - debug: 4.4.1 - docker-compose: 1.2.0 - dockerode: 4.0.7 + debug: 4.4.3 + docker-compose: 1.3.1 + dockerode: 4.0.9 get-port: 7.1.0 proper-lockfile: 4.1.2 properties-reader: 2.3.0 ssh-remote-port-forward: 1.0.4 - tar-fs: 3.1.0 + tar-fs: 3.1.1 tmp: 0.2.5 - undici: 7.13.0 + undici: 7.20.0 transitivePeerDependencies: + - bare-abort-controller - bare-buffer + - react-native-b4a - supports-color text-decoder@1.2.3: dependencies: - b4a: 1.6.7 + b4a: 1.7.3 + transitivePeerDependencies: + - react-native-b4a text-extensions@1.9.0: {} - text-table@0.2.0: {} + text-hex@1.0.0: {} - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 + text-table@0.2.0: {} - thenify@3.3.1: + thread-stream@3.1.0: dependencies: - any-promise: 1.3.0 + real-require: 0.2.0 through2@2.0.5: dependencies: @@ -12713,15 +13407,17 @@ snapshots: through@2.3.8: {} + timespan@2.3.0: {} + tiny-case@1.0.3: {} tinybench@2.9.0: {} tinyexec@0.3.2: {} - tinyglobby@0.2.14: + tinyglobby@0.2.15: dependencies: - fdir: 6.4.6(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 tinypool@1.1.1: {} @@ -12738,7 +13434,7 @@ snapshots: tmpl@1.0.5: {} - to-buffer@1.2.1: + to-buffer@1.2.2: dependencies: isarray: 2.0.5 safe-buffer: 5.2.1 @@ -12750,11 +13446,6 @@ snapshots: dependencies: kind-of: 3.2.2 - to-regex-range@2.1.1: - dependencies: - is-number: 3.0.0 - repeat-string: 1.6.1 - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -12766,8 +13457,6 @@ snapshots: regex-not: 1.0.2 safe-regex: 1.1.0 - toidentifier@1.0.0: {} - toidentifier@1.0.1: {} toml@3.0.0: {} @@ -12782,15 +13471,15 @@ snapshots: tree-kill@1.2.2: {} - trim-newlines@1.0.0: {} - trim-newlines@3.0.1: {} + triple-beam@1.4.1: {} + ts-api-utils@1.4.3(typescript@5.4.5): dependencies: typescript: 5.4.5 - ts-jest@29.4.1(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(esbuild@0.25.3)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.6.0)(ts-node@10.9.2(@types/node@20.6.0)(typescript@5.4.5)))(typescript@5.4.5): + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.6.0)(ts-node@10.9.2(@types/node@20.6.0)(typescript@5.4.5)))(typescript@5.4.5): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -12799,37 +13488,39 @@ snapshots: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.2 + semver: 7.7.4 type-fest: 4.41.0 typescript: 5.4.5 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.28.0 + '@babel/core': 7.29.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.0) - esbuild: 0.25.3 + babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 29.7.0 - ts-node-dev@1.1.6(typescript@5.4.5): + ts-node-dev@2.0.0(@types/node@20.6.0)(typescript@5.4.5): dependencies: chokidar: 3.6.0 - dateformat: 1.0.12 dynamic-dedupe: 0.3.0 minimist: 1.2.8 mkdirp: 1.0.4 - resolve: 1.22.10 + resolve: 1.22.11 rimraf: 2.7.1 source-map-support: 0.5.21 tree-kill: 1.2.2 - ts-node: 9.1.1(typescript@5.4.5) + ts-node: 10.9.2(@types/node@20.6.0)(typescript@5.4.5) tsconfig: 7.0.0 typescript: 5.4.5 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' ts-node@10.9.2(@types/node@20.6.0)(typescript@5.4.5): dependencies: '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 + '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 @@ -12838,21 +13529,21 @@ snapshots: acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 - diff: 4.0.2 + diff: 4.0.4 make-error: 1.3.6 typescript: 5.4.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@9.1.1(typescript@5.4.5): + tsc-alias@1.8.16: dependencies: - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - source-map-support: 0.5.21 - typescript: 5.4.5 - yn: 3.1.1 + chokidar: 3.6.0 + commander: 9.5.0 + get-tsconfig: 4.13.3 + globby: 11.1.0 + mylas: 2.1.14 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 tsconfig-paths@3.15.0: dependencies: @@ -12877,10 +13568,10 @@ snapshots: tslib@2.8.1: {} - tsx@4.19.3: + tsx@4.21.0: dependencies: - esbuild: 0.25.8 - get-tsconfig: 4.10.1 + esbuild: 0.27.3 + get-tsconfig: 4.13.6 optionalDependencies: fsevents: 2.3.3 @@ -12944,7 +13635,7 @@ snapshots: dependencies: content-type: 1.0.5 media-typer: 1.1.0 - mime-types: 3.0.1 + mime-types: 3.0.2 typed-array-buffer@1.0.3: dependencies: @@ -12952,29 +13643,31 @@ snapshots: es-errors: 1.3.0 is-typed-array: 1.1.15 - typeorm@0.3.20(ioredis@5.7.0)(pg@8.5.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.6.0)(typescript@5.4.5)): + typeorm@0.3.28(ioredis@5.9.2)(mongodb@6.21.0)(pg@8.5.1)(redis@4.7.0)(ts-node@10.9.2(@types/node@20.6.0)(typescript@5.4.5)): dependencies: '@sqltools/formatter': 1.2.5 + ansis: 4.2.0 app-root-path: 3.1.0 buffer: 6.0.3 - chalk: 4.1.2 - cli-highlight: 2.1.11 - dayjs: 1.11.13 - debug: 4.4.1 - dotenv: 16.4.5 - glob: 10.3.15 - mkdirp: 2.1.6 + dayjs: 1.11.19 + debug: 4.4.3 + dedent: 1.7.1 + dotenv: 16.6.1 + glob: 10.5.0 reflect-metadata: 0.2.2 sha.js: 2.4.12 + sql-highlight: 6.1.0 tslib: 2.8.1 - uuid: 9.0.1 + uuid: 11.1.0 yargs: 17.7.2 optionalDependencies: - ioredis: 5.7.0 + ioredis: 5.9.2 + mongodb: 6.21.0 pg: 8.5.1 redis: 4.7.0 ts-node: 10.9.2(@types/node@20.6.0)(typescript@5.4.5) transitivePeerDependencies: + - babel-plugin-macros - supports-color typescript@5.4.5: {} @@ -12985,7 +13678,9 @@ snapshots: undici-types@5.26.5: {} - undici@7.13.0: {} + undici-types@6.21.0: {} + + undici@7.20.0: {} union-value@1.0.1: dependencies: @@ -13005,9 +13700,9 @@ snapshots: untildify@4.0.0: {} - update-browserslist-db@1.1.3(browserslist@4.25.2): + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: - browserslist: 4.25.2 + browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 @@ -13017,6 +13712,11 @@ snapshots: urix@0.1.0: {} + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + use@3.1.1: {} util-deprecate@1.0.2: {} @@ -13025,6 +13725,10 @@ snapshots: uuid@10.0.0: {} + uuid@11.1.0: {} + + uuid@13.0.0: {} + uuid@8.3.2: {} uuid@9.0.1: {} @@ -13035,7 +13739,7 @@ snapshots: v8-to-istanbul@9.3.0: dependencies: - '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 @@ -13044,17 +13748,34 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - validator@13.15.15: {} + validator@13.15.26: {} vary@1.1.2: {} - vite-node@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1): + viem@2.45.1(typescript@5.4.5): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.4.5) + isows: 1.0.7(ws@8.18.3) + ox: 0.11.3(typescript@5.4.5) + ws: 8.18.3 + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + vite-node@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1) + vite: 6.4.1(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -13069,41 +13790,41 @@ snapshots: - tsx - yaml - vite@6.3.5(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1): + vite@6.4.1(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: - esbuild: 0.25.8 - fdir: 6.4.6(picomatch@4.0.3) + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.46.2 - tinyglobby: 0.2.14 + rollup: 4.59.0 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 20.6.0 fsevents: 2.3.3 - tsx: 4.19.3 - yaml: 2.8.1 + tsx: 4.21.0 + yaml: 2.8.2 - vitest@3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1): + vitest@3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 3.0.9 - '@vitest/mocker': 3.0.9(vite@6.3.5(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1)) + '@vitest/mocker': 3.0.9(vite@6.4.1(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.0.9 '@vitest/snapshot': 3.0.9 '@vitest/spy': 3.0.9 '@vitest/utils': 3.0.9 - chai: 5.2.1 - debug: 4.4.1 - expect-type: 1.2.2 - magic-string: 0.30.17 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 pathe: 2.0.3 - std-env: 3.9.0 + std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 0.3.2 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1) - vite-node: 3.0.9(@types/node@20.6.0)(tsx@4.19.3)(yaml@2.8.1) + vite: 6.4.1(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.0.9(@types/node@20.6.0)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.6.0 @@ -13123,9 +13844,9 @@ snapshots: wait-on@8.0.3: dependencies: - axios: 1.11.0 + axios: 1.13.5 joi: 17.13.3 - lodash: 4.17.21 + lodash: 4.17.23 minimist: 1.2.8 rxjs: 7.8.2 transitivePeerDependencies: @@ -13149,7 +13870,7 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 - which-typed-array@1.1.19: + which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 @@ -13170,6 +13891,26 @@ snapshots: why-is-node-running@3.2.2: {} + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + word-wrap@1.2.5: {} wordwrap@1.0.0: {} @@ -13188,12 +13929,14 @@ snapshots: wrap-ansi@8.1.0: dependencies: - ansi-styles: 6.2.1 + ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 wrappy@1.0.2: {} + wrench@1.3.9: {} + write-file-atomic@4.0.2: dependencies: imurmurhash: 0.1.4 @@ -13201,6 +13944,8 @@ snapshots: ws@8.17.1: {} + ws@8.18.3: {} + xmlhttprequest-ssl@2.0.0: {} xtend@4.0.2: {} @@ -13213,7 +13958,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.1: {} + yaml@2.8.2: {} yargs-parser@20.2.9: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 600b4bb48..35b7fe755 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,11 @@ packages: - - 'packages/**' + - packages/** + +ignoredBuiltDependencies: + - '@sentry/profiling-node' + - cpu-features + - esbuild + - msgpackr-extract + - protobufjs + - ssh2 + - yarn diff --git a/script.sh b/script.sh index 2da3ca39d..8cc4e104f 100755 --- a/script.sh +++ b/script.sh @@ -2,6 +2,8 @@ set -e +export DOCKER_API_VERSION="${DOCKER_API_VERSION:-1.44}" + # Ensure Docker network exists ensure_network() { local NETWORK_NAME="bako-network"