diff --git a/.env.example b/.env.example
index 9ead4ef0..19caf3d3 100644
--- a/.env.example
+++ b/.env.example
@@ -429,6 +429,16 @@ WS_DRAIN_TIMEOUT_MS=25000
# Empty disables JWT auth on the WS gateway.
AUTH_JWT_SECRET=
+# ─── Solver liveness heartbeats (issue #445) ─────────────────────────────────
+# Cadence clients heartbeat at (ms); a solver is auto-marked offline after
+# interval × misses without a beat (default 10 000 × 3 = 30 s).
+SOLVER_HEARTBEAT_INTERVAL_MS=10000
+SOLVER_HEARTBEAT_MISSES=3
+# Shared liveness store URL (defaults to REDIS_URL). Leave both empty for a
+# process-local store — single replica only; multi-replica deployments must
+# share liveness or each replica only sees its own heartbeats.
+SOLVER_HEARTBEAT_REDIS_URL=
+
# ─── API keys & distributed rate limiting (issue #441) ─────────────────────────
# How often (ms) the local rate-limiter fallback prunes expired window entries.
# Only relevant during a Redis outage; keeps the fallback map bounded.
@@ -549,6 +559,26 @@ DATASETS_STORAGE_KIND=memory
DATASETS_LOCAL_DIR=./data/datasets
+# ─── Solver reputation (issue #444) ──────────────────────────────────────────
+# Weights for each reputation sub-component. Sum must be exactly 1.0 in production;
+# dev/test renormalises any valid positive set so local experimentation doesn't
+# prevent boot.
+REP_WEIGHT_FILL_RATE=0.35
+REP_WEIGHT_LATENCY=0.15
+REP_WEIGHT_SLASHES=0.25
+REP_WEIGHT_QUOTE_HONOUR=0.15
+REP_WEIGHT_VOLUME=0.10
+# Shared exponential-decay half-life (seconds) for all events.
+# Default: 30 days — scores dominated by the last ~1 month of activity.
+REP_DECAY_HALFLIFE_SECONDS=2592000
+# Beta-distribution priors for the Bayesian cold-start lower bound on fill rate.
+# (α=4, β=1) ≈ prior mean 80%, 2.5% lower bound ~37%.
+REP_BAYES_ALPHA=4
+REP_BAYES_BETA=1
+# Volume-component knee in USD-equivalent notional; $100k default.
+REP_VOLUME_LAMBDA_USD=100000
+# How many trailing days of daily snapshots the /reputation endpoint exposes (1..365).
+REP_HISTORY_WINDOW_DAYS=30
# ─── Read replicas (#411) ─────────────────────────────────────────────────────
# Comma-separated read-replica Postgres connection strings.
# Leave blank to route all reads to the primary.
diff --git a/.env.mainnet.example b/.env.mainnet.example
index 7437e02e..c6d08dae 100644
--- a/.env.mainnet.example
+++ b/.env.mainnet.example
@@ -305,6 +305,16 @@ WS_DRAIN_TIMEOUT_MS=25000
# Empty disables JWT auth on the WS gateway.
AUTH_JWT_SECRET=
+# ─── Solver liveness heartbeats (issue #445) ─────────────────────────────────
+# Cadence clients heartbeat at (ms); a solver is auto-marked offline after
+# interval × misses without a beat (default 10 000 × 3 = 30 s).
+SOLVER_HEARTBEAT_INTERVAL_MS=10000
+SOLVER_HEARTBEAT_MISSES=3
+# Shared liveness store URL (defaults to REDIS_URL). Leave both empty for a
+# process-local store — single replica only; multi-replica deployments must
+# share liveness or each replica only sees its own heartbeats.
+SOLVER_HEARTBEAT_REDIS_URL=
+
# ─── API keys & distributed rate limiting (issue #441) ─────────────────────────
RATE_LIMIT_LOCAL_PRUNE_MS=60000
# Redis URL for the shared rate-limit window. Empty = bounded local limiter.
diff --git a/.env.staging.example b/.env.staging.example
index b687a823..986bacea 100644
--- a/.env.staging.example
+++ b/.env.staging.example
@@ -192,6 +192,16 @@ WS_DRAIN_TIMEOUT_MS=25000
# Empty disables JWT auth on the WS gateway.
AUTH_JWT_SECRET=
+# ─── Solver liveness heartbeats (issue #445) ─────────────────────────────────
+# Cadence clients heartbeat at (ms); a solver is auto-marked offline after
+# interval × misses without a beat (default 10 000 × 3 = 30 s).
+SOLVER_HEARTBEAT_INTERVAL_MS=10000
+SOLVER_HEARTBEAT_MISSES=3
+# Shared liveness store URL (defaults to REDIS_URL). Leave both empty for a
+# process-local store — single replica only; multi-replica deployments must
+# share liveness or each replica only sees its own heartbeats.
+SOLVER_HEARTBEAT_REDIS_URL=
+
# ─── API keys & distributed rate limiting (issue #441) ─────────────────────────
RATE_LIMIT_LOCAL_PRUNE_MS=60000
# Redis URL for the shared rate-limit window. Empty = bounded local limiter.
diff --git a/.env.testnet.example b/.env.testnet.example
index e87af4b2..3db33b8d 100644
--- a/.env.testnet.example
+++ b/.env.testnet.example
@@ -281,6 +281,16 @@ DATASETS_PUBLIC_BUCKET=
DATASETS_STORAGE_KIND=memory
DATASETS_LOCAL_DIR=
+# ─── Solver liveness heartbeats (issue #445) ─────────────────────────────────
+# Cadence clients heartbeat at (ms); a solver is auto-marked offline after
+# interval × misses without a beat (default 10 000 × 3 = 30 s).
+SOLVER_HEARTBEAT_INTERVAL_MS=10000
+SOLVER_HEARTBEAT_MISSES=3
+# Shared liveness store URL (defaults to REDIS_URL). Leave both empty for a
+# process-local store — single replica only; multi-replica deployments must
+# share liveness or each replica only sees its own heartbeats.
+SOLVER_HEARTBEAT_REDIS_URL=
+
# ─── Health probes (issue #492) ──────────────────────────────────────────────
# Roles served by this process (api, ws, worker); readiness checks follow them.
SERVICE_ROLES=api,ws,worker
diff --git a/.eslintrc.json b/.eslintrc.json
index 09d27129..233714eb 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -52,7 +52,7 @@
"ignorePatterns": ["dist", "node_modules"],
"overrides": [
{
- "files": ["scripts/**/*.ts", "tools/**/*.ts"],
+ "files": ["scripts/**/*.ts", "tools/**/*.ts", "test/chaos/**/*.ts"],
"rules": {
"no-restricted-syntax": "off"
}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e69de29b..e53cbef6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,1236 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+# Cancel in-progress runs for the same branch/PR
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ secrets-scan:
+ name: Secrets Scanning (Gitleaks)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ with:
+ fetch-depth: 0
+ - name: Gitleaks Scan
+ uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ vulnerability-scan:
+ name: Dependency Vulnerability Audit
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+ cache: npm
+ - run: npm audit --audit-level=high
+
+ license-scan:
+ name: Dependency License Compliance
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+ cache: npm
+ - name: Install dependencies
+ run: npm ci
+ # Permissive-only allow-list, reconciled with the project's own
+ # MIT/Apache-2.0 license (issue #107). Fails the build on any
+ # dependency (direct or transitive) carrying a copyleft or
+ # unrecognized/unlicensed license.
+ - name: Check dependency licenses
+ run: |
+ npx license-checker --production \
+ --onlyAllow 'MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD;CC0-1.0;Unlicense' \
+ --excludePrivatePackages
+
+ commitlint:
+ name: Commit Message Lint
+ runs-on: ubuntu-latest
+ # Only meaningful on PRs — on direct pushes to main we skip this check
+ # since squash merges are handled by the branch protection rule.
+ if: github.event_name == 'pull_request'
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ with:
+ # Fetch enough history to validate all commits in the PR.
+ fetch-depth: 0
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ # Lint every commit in the PR from the merge base to HEAD.
+ - name: Lint commit messages
+ run: |
+ npx commitlint \
+ --from ${{ github.event.pull_request.base.sha }} \
+ --to ${{ github.event.pull_request.head.sha }} \
+ --verbose
+
+ backend:
+ # The en dash in the name is not cosmetic: branch protection stores the
+ # exact check name as a string, so replacing it with a hyphen renames a
+ # required check and blocks every PR until an admin edits the rule.
+ name: Backend (Nest) – Node ${{ matrix.node-version }}
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [20, 22]
+
+ # Spin up a PostgreSQL service container so Prisma can connect during the
+ # migrate step (migrate deploy requires a live DB).
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_USER: vortex
+ POSTGRES_PASSWORD: vortex
+ POSTGRES_DB: vortex
+ ports:
+ - 5432:5432
+ # Wait until postgres is healthy before the steps run.
+ options: >-
+ --health-cmd="pg_isready -U vortex"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=5
+
+ env:
+ DATABASE_URL: postgresql://vortex:vortex@localhost:5432/vortex?schema=public
+
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+
+ # ── Node / npm cache (#130) ──────────────────────────────────────────
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ # ── Env drift check ────────────────────────────────────────────────────
+ # Fast, early-failing check that env.validation.ts, .env*.example, and
+ # configuration.ts's process.env reads all agree on the same variable set.
+ - name: Check env var drift
+ run: npx tsx scripts/check-env-drift.ts
+
+ # ── Prisma ──────────────────────────────────────────────────────────────
+ - name: Validate Prisma schema
+ run: npm run db:validate
+
+ # `npm ci` deletes node_modules, so the generated client has to be cached
+ # *after* the install, not before. The key includes package-lock.json as
+ # well as the schema: the client is also regenerated when the Prisma
+ # version moves, and a client generated by a different version fails at
+ # runtime rather than at generate time.
+ - name: Restore cached Prisma client
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ with:
+ path: node_modules/.prisma
+ key: prisma-${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('prisma/schema.prisma', 'package-lock.json') }}
+
+ - name: Generate Prisma client
+ run: npm run db:generate
+
+ # Apply all pending migrations to the CI database so the schema stays in
+ # sync and any broken migration SQL is caught before merge.
+ - name: Run database migrations
+ run: npm run db:migrate:prod
+
+ # ── Build & static checks ──────────────────────────────────────────────
+ - name: Lint
+ run: npm run lint
+
+ - name: Type-check
+ run: npm run typecheck
+
+ # dist/ is keyed on everything tsc reads, so a hit means the restored
+ # output is what this commit's build would produce anyway. There is
+ # deliberately no restore-keys prefix fallback: a partially-restored
+ # dist/ from a different commit is exactly the stale-artifact bug this
+ # cache must not be able to introduce.
+ - name: Restore cached build output
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ with:
+ path: dist
+ key: dist-${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('src/**/*.ts', 'package.json', 'tsconfig.json', 'nest-cli.json') }}
+
+ - name: Build
+ run: npm run build
+
+ # Unit and E2E tests deliberately do NOT run here. They are sharded into
+ # the `unit-tests`, `e2e-tests` and `coverage` jobs below (issue #486) so
+ # the suite fans out across runners instead of serialising behind lint
+ # and build on both Node versions. This job's name is a required status
+ # check, so it stays exactly as it was and keeps meaning "the backend
+ # compiles and passes static analysis on Node X".
+
+ dast:
+ name: DAST API scan (OWASP ZAP)
+ runs-on: ubuntu-latest
+ needs: backend
+ timeout-minutes: 15
+ env:
+ DATABASE_URL: postgresql://vortex:vortex@localhost:5432/vortex?schema=public
+ NODE_ENV: test
+ PORT: 4000
+ CORS_ORIGIN: "*"
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_USER: vortex
+ POSTGRES_PASSWORD: vortex
+ POSTGRES_DB: vortex
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd="pg_isready -U vortex"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=5
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+ cache: npm
+ - name: Install dependencies
+ run: npm ci
+ - name: Generate Prisma client
+ run: npm run db:generate
+ - name: Run database migrations
+ run: npm run db:migrate:prod
+ - name: Build backend
+ run: npm run build
+ - name: Start backend for ZAP scan
+ run: >-
+ npm run start > /tmp/vortex-backend.log 2>&1 &
+ - name: Wait for backend to become healthy
+ run: |
+ for i in $(seq 1 60); do
+ if curl -fsS http://localhost:${PORT}/health >/dev/null; then
+ echo "Backend healthy"; exit 0;
+ fi
+ sleep 2;
+ done
+ echo "Backend never became healthy" >&2
+ cat /tmp/vortex-backend.log || true
+ exit 1
+ - name: Run OWASP ZAP API scan
+ run: |
+ docker run --rm \
+ --network host \
+ -v "$PWD/.github/zap:/zap/wrk:ro" \
+ -v "$PWD:/zap/output:rw" \
+ owasp/zap2docker-stable \
+ zap-api-scan.py \
+ -t http://localhost:${PORT}/docs-json \
+ -f openapi \
+ -m 10 \
+ -J /zap/output/zap-report.json \
+ -r /zap/output/zap-report.html \
+ -x /zap/output/zap-report.xml \
+ \
+ || true
+ - name: Fail on medium+ findings
+ run: |
+ python - <<'PY'
+ import json, os, sys
+ path = "zap-report.json"
+ if not os.path.exists(path):
+ print("No ZAP report generated; failing the job.")
+ sys.exit(1)
+ data = json.load(open(path, encoding="utf-8"))
+ findings = []
+ for site in data.get("site", []):
+ for alert in site.get("alerts", []):
+ risk = int(alert.get("riskcode", "0"))
+ if risk >= 2:
+ findings.append({
+ "name": alert.get("name"),
+ "risk": alert.get("riskdesc", "Unknown"),
+ "url": alert.get("url", "unknown"),
+ })
+ rules = json.load(open("zap-report.json", encoding="utf-8")) if False else None
+ if findings:
+ print("Medium+ ZAP findings detected:")
+ for item in findings:
+ print(f"- {item['name']} ({item['risk']}) @ {item['url']}")
+ sys.exit(1)
+ print("No medium+ ZAP findings detected.")
+ PY
+ - name: Convert ZAP results to SARIF
+ if: always()
+ run: |
+ python - <<'PY'
+ import json
+ import os
+ src = "zap-report.json"
+ out = "zap-report.sarif"
+ if not os.path.exists(src):
+ print("No ZAP results to convert; writing an empty SARIF file.")
+ empty = {
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
+ "version": "2.1.0",
+ "runs": [{
+ "tool": {"driver": {"name": "OWASP ZAP", "informationUri": "https://www.zaproxy.org/", "rules": []}},
+ "results": []
+ }]
+ }
+ json.dump(empty, open(out, "w", encoding="utf-8"))
+ raise SystemExit(0)
+ data = json.load(open(src, encoding="utf-8"))
+ results = []
+ rules = []
+ seen = set()
+ for site in data.get("site", []):
+ for alert in site.get("alerts", []):
+ rule_id = alert.get("pluginid", alert.get("alert", "unknown"))
+ if rule_id not in seen:
+ seen.add(rule_id)
+ rules.append({
+ "id": str(rule_id),
+ "shortDescription": {"text": alert.get("name", "OWASP ZAP alert")},
+ "fullDescription": {"text": alert.get("desc", "Alert reported by OWASP ZAP")},
+ "defaultConfiguration": {"level": "warning"},
+ "helpUri": "https://www.zaproxy.org/docs/alerts/"
+ })
+ level = "warning"
+ risk = alert.get("riskdesc", "Informational")
+ if "High" in risk:
+ level = "error"
+ elif "Medium" in risk:
+ level = "warning"
+ results.append({
+ "ruleId": str(rule_id),
+ "level": level,
+ "message": {"text": alert.get("name", "OWASP ZAP alert")},
+ "locations": [{"physicalLocation": {"artifactLocation": {"uri": alert.get("url", "http://localhost")}}}],
+ "properties": {"issue_confidence": alert.get("confidence", "Medium")}
+ })
+ sarif = {
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
+ "version": "2.1.0",
+ "runs": [{
+ "tool": {"driver": {"name": "OWASP ZAP", "informationUri": "https://www.zaproxy.org/", "rules": rules}},
+ "results": results
+ }]
+ }
+ json.dump(sarif, open(out, "w", encoding="utf-8"))
+ PY
+ - name: Upload SARIF to GitHub code scanning
+ if: always()
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: zap-report.sarif
+
+ # ── Sharded test fan-out (issue #486) ───────────────────────────────────
+ # Three jobs replace the old `npm test` + `npm run test:e2e` pair:
+ # unit-tests -> N Jest shards, each writing its own coverage + flake report
+ # e2e-tests -> M Jest shards against the migrated Postgres service
+ # coverage -> merges the shard reports and enforces the 70% gate
+ #
+ # fail-fast is off on both matrices on purpose: with it on, the first failing
+ # shard cancels its siblings, their coverage artifacts are never uploaded,
+ # and the merge job reports "a shard never wrote its report" instead of the
+ # real failure. Every shard has to finish so the coverage merge is complete.
+
+ unit-tests:
+ name: Unit tests (shard ${{ matrix.shard }}/${{ matrix.shard-total }})
+ runs-on: ubuntu-latest
+ needs: backend
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [1, 2, 3, 4]
+ shard-total: [4]
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_USER: vortex
+ POSTGRES_PASSWORD: vortex
+ POSTGRES_DB: vortex
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd="pg_isready -U vortex"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=5
+ env:
+ DATABASE_URL: postgresql://vortex:vortex@localhost:5432/vortex?schema=public
+ # Opts the Postgres repository contract suite in
+ # (src/intents/prisma-intents.repository.spec.ts — issue #404).
+ TEST_DATABASE_URL: postgresql://vortex:vortex@localhost:5432/vortex?schema=public
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ # Tests run on the lowest supported Node only. Running the same 35
+ # spec files on 20 and 22 doubles CI minutes to re-test runtime
+ # behaviour the build job already proves compiles on both; the
+ # version-specific risk is a native module, which `npm run build`
+ # loads on both anyway.
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ # Same Prisma client cache as the backend job: generate-and-migrate is
+ # the slowest fixed cost in a test shard, and the key is content-addressed
+ # so a schema or lockfile change invalidates it on its own.
+ - name: Restore cached Prisma client
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ with:
+ path: node_modules/.prisma
+ key: prisma-${{ runner.os }}-node20-${{ hashFiles('prisma/schema.prisma', 'package-lock.json') }}
+
+ - name: Generate Prisma client
+ run: npm run db:generate
+
+ - name: Run database migrations
+ run: npm run db:migrate:prod
+
+ # run-tests.mjs applies test/quarantine.json, writes the shard's
+ # Istanbul report to its own directory (so shards cannot clobber each
+ # other), and retries only the failing files to separate a flake from a
+ # real failure.
+ - name: Run unit test shard
+ run: >-
+ node scripts/ci/run-tests.mjs
+ --suite=unit
+ --shard=${{ matrix.shard }}/${{ matrix.shard-total }}
+ --out-dir=ci-artifacts/unit-s${{ matrix.shard }}
+ --coverage-dir=coverage-shards/unit-s${{ matrix.shard }}
+
+ - name: Upload shard coverage
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: coverage-unit-s${{ matrix.shard }}
+ path: coverage-shards/unit-s${{ matrix.shard }}
+ if-no-files-found: warn
+
+ - name: Upload flake report and Jest results
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: unit-s${{ matrix.shard }}-logs
+ path: ci-artifacts/unit-s${{ matrix.shard }}
+ if-no-files-found: warn
+
+ e2e-tests:
+ name: E2E tests (shard ${{ matrix.shard }}/${{ matrix.shard-total }})
+ runs-on: ubuntu-latest
+ needs: backend
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [1, 2]
+ shard-total: [2]
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_USER: vortex
+ POSTGRES_PASSWORD: vortex
+ POSTGRES_DB: vortex
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd="pg_isready -U vortex"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=5
+ env:
+ DATABASE_URL: postgresql://vortex:vortex@localhost:5432/vortex?schema=public
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ # Same Prisma client cache as the backend job: generate-and-migrate is
+ # the slowest fixed cost in a test shard, and the key is content-addressed
+ # so a schema or lockfile change invalidates it on its own.
+ - name: Restore cached Prisma client
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ with:
+ path: node_modules/.prisma
+ key: prisma-${{ runner.os }}-node20-${{ hashFiles('prisma/schema.prisma', 'package-lock.json') }}
+
+ - name: Generate Prisma client
+ run: npm run db:generate
+
+ - name: Run database migrations
+ run: npm run db:migrate:prod
+
+ # Anvil for the EVM deposit-verification integration test (issue #403,
+ # test/evm/deposit-verifier.anvil.test.ts — skipped when anvil is absent).
+ - name: Install Foundry (anvil)
+ uses: foundry-rs/foundry-toolchain@v1
+
+ # No --coverage-dir here. test/jest-e2e.json has no collectCoverageFrom,
+ # so its coverage map has different file keys than the unit config's;
+ # merging the two would trip the shard-consistency check in
+ # coverage-merge.mjs. The coverage gate is measured on the unit shards,
+ # which is also where nearly all of the code is exercised.
+ - name: Run E2E test shard
+ run: >-
+ node scripts/ci/run-tests.mjs
+ --suite=e2e
+ --shard=${{ matrix.shard }}/${{ matrix.shard-total }}
+ --out-dir=ci-artifacts/e2e-s${{ matrix.shard }}
+
+ - name: Upload flake report and Jest results
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: e2e-s${{ matrix.shard }}-logs
+ path: ci-artifacts/e2e-s${{ matrix.shard }}
+ if-no-files-found: warn
+
+ # ── E2E against the Postgres-backed intents store (issue #404) ─────────────
+ # The e2e-tests shards use the default in-memory store. This runs the same
+ # suite, including the concurrency and POST /intents latency load tests,
+ # with INTENTS_STORE=postgres. --runInBand: suites share one database, so
+ # parallel workers would race on table-wide counts (e.g. the stats deltas).
+ e2e-postgres-store:
+ name: E2E tests (INTENTS_STORE=postgres)
+ runs-on: ubuntu-latest
+ needs: backend
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_USER: vortex
+ POSTGRES_PASSWORD: vortex
+ POSTGRES_DB: vortex
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd="pg_isready -U vortex"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=5
+ env:
+ DATABASE_URL: postgresql://vortex:vortex@localhost:5432/vortex?schema=public
+ INTENTS_STORE: postgres
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Generate Prisma client
+ run: npm run db:generate
+
+ - name: Run database migrations
+ run: npm run db:migrate:prod
+
+ - name: Install Foundry (anvil)
+ uses: foundry-rs/foundry-toolchain@v1
+
+ - name: E2E tests
+ run: npm run test:e2e -- --runInBand
+
+ coverage:
+ name: Coverage merge and gate
+ runs-on: ubuntu-latest
+ needs: [unit-tests, e2e-tests]
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ # Without merge-multiple, each artifact lands in its own subdirectory of
+ # the target path, so the four shards arrive as
+ # coverage-shards/coverage-unit-s1..4/coverage-final.json -- exactly the
+ # one-directory-deep layout coverage-merge.mjs scans.
+ - name: Download shard coverage
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ pattern: coverage-unit-s*
+ path: coverage-shards
+
+ - name: Download flake reports
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ pattern: "*-logs"
+ path: ci-artifacts
+
+ # The 70% gate is enforced here, on the union, and never per shard: a
+ # shard that runs a ninth of the suite cannot meet a global threshold.
+ # The threshold itself is read from jest.config.js, so this stays a single
+ # definition rather than a second copy in the workflow.
+ - name: Merge shard coverage and enforce threshold
+ run: >-
+ node scripts/ci/coverage-merge.mjs
+ --shards=coverage-shards
+ --out=coverage
+ --expect-shards=4
+ --title="Coverage (merged from 4 unit test shards)"
+
+ - name: Upload merged coverage
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: coverage-merged
+ path: coverage
+ if-no-files-found: warn
+
+ # Pure Node, no npm ci: the quarantine file is the one piece of test
+ # infrastructure that can be validated before dependencies are installed, so
+ # it is also usable as a pre-commit guard (scripts/ci/check-quarantine.mjs).
+ test-hygiene:
+ name: Quarantine file hygiene
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+
+ # Fails on a missing owner/issue, a path that no longer exists, a
+ # duplicate entry, or an entry older than staleAfterDays.
+ - name: Validate test/quarantine.json
+ run: node scripts/ci/check-quarantine.mjs
+
+ # ── TLA+ model checking (issue #471) ─────────────────────────────────────
+ # Bounded TLC run on changes to the spec or the canonical state machine.
+ formal:
+ name: TLA+ model check (TLC, bounded)
+ runs-on: ubuntu-latest
+ if: >-
+ github.event_name == 'pull_request' &&
+ (contains(join(github.event.pull_request.labels.*.name, ','), 'formal') ||
+ true)
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ - name: Run TLC (bounded model)
+ uses: docker://tlaplus/tlaplus:latest
+ with:
+ args: tlc -config formal/IntentLifecycle.cfg formal/IntentLifecycle.tla
+ - name: Upload TLC output
+ if: always()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: tlc-output
+ path: formal/*.out
+
+ # ── Semgrep custom rules (issue #478) ────────────────────────────────────
+ semgrep:
+ name: Semgrep custom rules
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ - uses: returntocorp/semgrep-action@713efdd345f3035192eaa63f56867b88e63e4e5d # v1
+ with:
+ config: .semgrep/rules
+ error: true
+ - name: Semgrep rule tests
+ run: |
+ pip install semgrep
+ semgrep --test --config .semgrep/rules .semgrep/tests
+
+ # ── Prometheus rules (issue #480) ────────────────────────────────────────
+ promtool:
+ name: promtool check + test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ - name: Install promtool
+ run: |
+ curl -sL https://github.com/prometheus/prometheus/releases/download/v2.53.0/prometheus-2.53.0.linux-amd64.tar.gz | tar xz
+ sudo mv prometheus-2.53.0.linux-amd64/promtool /usr/local/bin/
+ - name: Check rules
+ run: promtool check rules ops/prometheus/rules/vortex-slo.yml ops/prometheus/rules/vortex-canary.yml
+ - name: Unit-test rules
+ run: promtool test rules ops/prometheus/rules/vortex-slo_test.yml ops/prometheus/rules/vortex-canary_test.yml
+ - name: Check rule file is also a valid Prometheus config fragment
+ run: promtool check config ops/prometheus/prometheus.yml
+
+ # ── Grafana dashboards (issue #481) ───────────────────────────────────────
+ # Two things are checked, neither of which needs a Grafana instance:
+ # 1. build.mjs --check proves the committed dashboard JSON is exactly what
+ # the generator produces, so a hand-edited panel cannot drift.
+ # 2. validate.mjs proves the panels are usable: every recording rule and
+ # metric a panel names exists, every panel has a legend, description,
+ # unit and threshold set, no panel reintroduces a multi-day
+ # histogram_quantile, and every alert links to a dashboard that exists.
+ #
+ # Both are plain Node with no dependencies, which is the whole point of
+ # generating the JSON ourselves instead of adding grafonnet to the toolchain.
+ grafana:
+ name: Grafana dashboards
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+
+ - name: Generated dashboards are in sync with build.mjs
+ run: node ops/grafana/build.mjs --check
+
+ - name: Validate dashboards, panels and alert links
+ run: node ops/grafana/validate.mjs
+
+ - name: Validate the observability Compose profile
+ run: docker compose --profile observability config --quiet
+
+ - name: Upload dashboards for review
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: grafana-dashboards
+ path: ops/grafana/dashboards
+
+ # ── Synthetic canary against the docker-compose stack (issue #496) ────────
+ canary:
+ name: Canary – local lifecycle run (docker-compose)
+ runs-on: ubuntu-latest
+ needs: backend
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+ cache: npm
+ - run: npm ci
+ - name: Generate throwaway canary keys
+ run: |
+ node -e '
+ const { Keypair } = require("@stellar/stellar-sdk");
+ const u = Keypair.random(), s = Keypair.random();
+ console.log(`CANARY_USER_SECRET=${u.secret()}`);
+ console.log(`CANARY_SOLVER_SECRET=${s.secret()}`);
+ console.log(`CANARY_ADDRESSES=${u.publicKey()},${s.publicKey()}`);
+ ' >> "$GITHUB_ENV"
+ - name: Start stack
+ run: docker compose up -d --build --wait postgres redis && docker compose up -d app
+ - name: Wait for API
+ run: timeout 300 sh -c 'until curl -sf http://localhost:4000/health/live; do sleep 5; done'
+ - name: Run canary once
+ env:
+ CANARY_SETTLE_ONCHAIN: "false"
+ run: npm run canary -- --once
+ - name: Stack logs
+ if: failure()
+ run: docker compose logs app
+
+ # ── Migration rollback verification ────────────────────────────────────────
+ # For every prisma/migrations/*/down.sql, applies migration.sql then
+ # down.sql against a fresh Postgres and asserts the schema returns to its
+ # pre-migration (empty) state. See prisma/migrations/README.md.
+ migration-rollback:
+ name: Migration rollback verification
+ runs-on: ubuntu-latest
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_USER: vortex
+ POSTGRES_PASSWORD: vortex
+ POSTGRES_DB: vortex_rollback_test
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ env:
+ PGHOST: localhost
+ PGPORT: 5432
+ PGUSER: vortex
+ PGPASSWORD: vortex
+ PGDATABASE: vortex_rollback_test
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+
+ - name: Apply each migration's up then down script and verify schema is restored
+ run: |
+ set -euo pipefail
+ for dir in prisma/migrations/*/; do
+ name=$(basename "$dir")
+ up="$dir/migration.sql"
+ down="$dir/down.sql"
+ [ -f "$down" ] || continue
+
+ echo "::group::$name"
+ before=$(psql -Atc "select tablename from pg_tables where schemaname='public' order by 1")
+ psql -v ON_ERROR_STOP=1 -f "$up"
+ psql -v ON_ERROR_STOP=1 -f "$down"
+ after=$(psql -Atc "select tablename from pg_tables where schemaname='public' order by 1")
+
+ if [ "$before" != "$after" ]; then
+ echo "Schema after down.sql does not match pre-migration state for $name"
+ echo "before: $before"
+ echo "after: $after"
+ exit 1
+ fi
+ echo "::endgroup::"
+ done
+
+ # ── Migration lint ─────────────────────────────────────────────────────────
+ # Blocks unsafe DDL in migrations this change adds or modifies: non-concurrent
+ # index builds/drops, column type rewrites, NOT NULL without a default,
+ # lock-heavy constraints, LOCK TABLE. Also requires a down.sql in every such
+ # migration. Untouched migrations aren't linted, so older ones can't fail
+ # retroactively. The rules and the `-- squawk-ignore` override are in
+ # prisma/migrations/README.md.
+ migration-lint:
+ name: Migration lint (squawk)
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ # Full history so the diff base below is always present.
+ fetch-depth: 0
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ # --ignore-scripts skips Prisma's engine download and the husky hook;
+ # neither is needed here, and it keeps the job well under 2 minutes.
+ - name: Install dependencies
+ run: npm ci --ignore-scripts
+
+ - name: Test the migration checker (fixtures)
+ run: npm run test:scripts
+
+ - name: Lint changed migrations
+ env:
+ EVENT_NAME: ${{ github.event_name }}
+ BEFORE_SHA: ${{ github.event.before }}
+ run: |
+ # Pull requests: GitHub checks out a merge commit whose first parent is
+ # the base branch tip, so HEAD^1 is exactly "everything this PR adds".
+ # Pushes: diff against the commit that was on the branch before the push.
+ if [ "$EVENT_NAME" = "pull_request" ] || [ -z "$BEFORE_SHA" ] || [ -z "${BEFORE_SHA//0/}" ]; then
+ base="HEAD^1"
+ else
+ base="$BEFORE_SHA"
+ fi
+ npm run check:migrations -- --base "$base"
+
+ # ── N-1 app against N schema (issue #497) ─────────────────────────────────
+ # The issue's compatibility criterion: "previous app version's e2e smoke
+ # suite runs against the migrated schema in CI". Concretely, this job:
+ #
+ # 1. applies the migrations of THIS revision (schema N) to a fresh Postgres;
+ # 2. checks out the previous revision of the app (N-1) into a second
+ # directory and installs its own dependencies there;
+ # 3. runs three things from N-1 against schema N:
+ # a. `prisma migrate deploy` — N-1's migration set must reconcile
+ # against the N database. This is the exact "misordered migration"
+ # failure the issue describes: if a migration was rewritten,
+ # renamed or re-ordered between N-1 and N, this fails here instead
+ # of in production;
+ # b. a raw query through N-1's *generated Prisma client* — the e2e
+ # harness (test/utils/create-test-app.ts) deliberately MOCKS
+ # PrismaService, so the smoke specs below boot the N-1 app but do
+ # not touch the database. This probe is what makes the run
+ # schema-sensitive;
+ # c. N-1's e2e smoke subset (health + OpenAPI contract), when those
+ # specs exist at that revision.
+ #
+ # Which revision counts as "previous"? Chosen per event so that N-1 is the
+ # app that is actually running/deployed, not an arbitrary ancestor:
+ # * push: `github.event.before` — literally the tip that was on
+ # the branch before this push, i.e. what staging runs. On
+ # main that is the deployed revision by definition.
+ # (`HEAD^1` would be wrong for multi-commit pushes: it
+ # points *inside* the pushed range, at something never
+ # deployed.)
+ # * pull_request: the merge commit's first parent, i.e. the base-branch
+ # tip this PR is merged onto — what the branch runs. Only
+ # if that is not an ancestor of origin/main (checkout fell
+ # back to the head commit because the merge ref was
+ # unavailable) do we fall back to the merge-base with
+ # origin/main; an older base only makes the test stricter,
+ # never weaker.
+ # * first push / unreachable object: skip gracefully with an explicit
+ # ::notice:: — there is no N-1 to test, and failing the build for that
+ # would block the very first commit of a branch.
+ migration-compat:
+ name: Migration compatibility (N-1 app vs N schema)
+ runs-on: ubuntu-latest
+ # Hard budget: the issue asks for "under ~10 minutes". The two installs
+ # (current + N-1 worktree) dominate; everything else is seconds.
+ timeout-minutes: 10
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_USER: vortex
+ POSTGRES_PASSWORD: vortex
+ POSTGRES_DB: vortex
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd="pg_isready -U vortex"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=5
+ env:
+ DATABASE_URL: postgresql://vortex:vortex@localhost:5432/vortex?schema=public
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ with:
+ # Full history: resolving `before`, the merge-base and the worktree
+ # checkout all need commits that a shallow clone would omit.
+ fetch-depth: 0
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies (current revision)
+ run: npm ci
+
+ - name: Generate Prisma client
+ run: npm run db:generate
+
+ # ── Step 1: bring the database to schema N ────────────────────────────
+ - name: Apply this revision's migrations (schema N)
+ run: npm run db:migrate:prod
+
+ # ── Step 2: pick the previous revision, or skip with a notice ─────────
+ - name: Resolve the previous revision (N-1)
+ id: prev
+ env:
+ EVENT_NAME: ${{ github.event_name }}
+ BEFORE_SHA: ${{ github.event.before }}
+ BASE_REF: ${{ github.base_ref }}
+ run: |
+ set -euo pipefail
+ prev=""
+ case "$EVENT_NAME" in
+ push)
+ # `before` is all-zeros on the first push of a ref and can be
+ # missing after a force-push; both are handled by the guards.
+ if [ -n "${BEFORE_SHA:-}" ] && [ "${BEFORE_SHA//0/}" != "" ] \
+ && git cat-file -e "${BEFORE_SHA}^{commit}" 2>/dev/null; then
+ prev="$BEFORE_SHA"
+ fi
+ ;;
+ pull_request)
+ # Merge ref: HEAD has two parents, first one is the base tip.
+ if git rev-parse --verify --quiet HEAD^2 >/dev/null \
+ && git cat-file -e "HEAD^1^{commit}" 2>/dev/null \
+ && git merge-base --is-ancestor "HEAD^1" "origin/${BASE_REF}"; then
+ prev=$(git rev-parse "HEAD^1")
+ else
+ # Fallback: the shared ancestor with the integration branch.
+ prev=$(git merge-base HEAD "origin/${BASE_REF}" 2>/dev/null || true)
+ fi
+ ;;
+ esac
+ if [ -z "$prev" ] || ! git cat-file -e "${prev}^{commit}" 2>/dev/null; then
+ echo "available=false" >> "$GITHUB_OUTPUT"
+ echo "::notice::No usable previous revision for this event — skipping the N-1 compatibility check (first push of the ref, force-push, or unrelated history). Nothing to compare against, so there is nothing to fail."
+ exit 0
+ fi
+ if [ "$prev" = "$(git rev-parse HEAD)" ]; then
+ echo "available=false" >> "$GITHUB_OUTPUT"
+ echo "::notice::Previous revision equals HEAD — skipping the N-1 compatibility check (degenerate history)."
+ exit 0
+ fi
+ echo "sha=$prev" >> "$GITHUB_OUTPUT"
+ echo "available=true" >> "$GITHUB_OUTPUT"
+ echo "N-1 revision: $prev ($(git log -1 --format=%s "$prev" | cut -c1-72))"
+
+ # ── Step 3: materialise N-1 in a second directory ─────────────────────
+ # A `git worktree` shares the object database with the checkout above
+ # (no second clone), while giving N-1 its own package.json, lockfile and
+ # node_modules — the current tree stays untouched for the schema-N side.
+ # It lives under $RUNNER_TEMP so it cannot be mistaken for workspace
+ # content by later steps or by the artifact uploaders.
+ - name: Check out N-1 into a separate worktree
+ if: steps.prev.outputs.available == 'true'
+ id: worktree
+ run: |
+ set -euo pipefail
+ N1_DIR="$RUNNER_TEMP/n1-app"
+ git worktree add --detach "$N1_DIR" "${{ steps.prev.outputs.sha }}"
+ echo "N1_DIR=$N1_DIR" >> "$GITHUB_ENV"
+ echo "Checked out N-1 at $N1_DIR"
+
+ - name: Install N-1 dependencies
+ if: steps.prev.outputs.available == 'true'
+ working-directory: ${{ env.N1_DIR }}
+ run: npm ci --no-audit --no-fund
+
+ # @prisma/client's postinstall normally generates the client, but that is
+ # an implicit behaviour of whatever Prisma version N-1 pinned; make it
+ # explicit (and skip it cleanly on revisions whose package.json has no
+ # such script yet).
+ - name: Generate N-1 Prisma client
+ if: steps.prev.outputs.available == 'true'
+ working-directory: ${{ env.N1_DIR }}
+ run: |
+ if node -e 'process.exit(require("./package.json").scripts?.["db:generate"] ? 0 : 1)'; then
+ npm run db:generate
+ else
+ echo "No db:generate script at this revision; relying on @prisma/client postinstall."
+ fi
+
+ # ── Step 4: the actual compatibility probes ───────────────────────────
+ # (a) N-1's migration set must reconcile with schema N — the ordering /
+ # rewriting guard.
+ - name: N-1 `prisma migrate deploy` against schema N
+ if: steps.prev.outputs.available == 'true'
+ working-directory: ${{ env.N1_DIR }}
+ run: npx prisma migrate deploy
+
+ # (b) The schema-sensitive probe: query through N-1's generated client.
+ # Written as a file (not `node -e`) so quoting cannot eat the SQL, and
+ # placed inside the worktree so `require("@prisma/client")` resolves
+ # N-1's client, not the current revision's.
+ - name: Query schema N through the N-1 Prisma client
+ if: steps.prev.outputs.available == 'true'
+ working-directory: ${{ env.N1_DIR }}
+ run: |
+ set -euo pipefail
+ cat > compat-smoke.js <<'JS'
+ const { PrismaClient } = require("@prisma/client");
+ (async () => {
+ const prisma = new PrismaClient();
+ const tables = await prisma.$queryRawUnsafe(
+ "SELECT count(*)::int AS n FROM information_schema.tables WHERE table_schema = 'public'",
+ );
+ const n = Number(tables[0].n);
+ if (n < 1) throw new Error("schema N shows no public tables");
+ // The table every released revision since init depends on.
+ await prisma.$queryRawUnsafe('SELECT count(*)::int FROM "intents"');
+ console.log(`N-1 client queried schema N: ${n} public tables, intents readable`);
+ await prisma.$disconnect();
+ })().catch((err) => {
+ console.error("N-1 client cannot use schema N:", err);
+ process.exit(1);
+ });
+ JS
+ node compat-smoke.js
+
+ # (c) N-1's e2e smoke subset. Existence is checked per file because the
+ # two specs may not have existed at every historical revision — missing
+ # coverage at N-1 is a reason to notice, not to fail the build.
+ - name: Run N-1 e2e smoke suite against schema N
+ if: steps.prev.outputs.available == 'true'
+ working-directory: ${{ env.N1_DIR }}
+ run: |
+ set -euo pipefail
+ smoke=()
+ for f in test/health.e2e-spec.ts test/openapi-contract.e2e-spec.ts; do
+ if [ -f "$f" ]; then
+ smoke+=("$f")
+ else
+ echo "::notice::$f does not exist at the N-1 revision — skipping that spec."
+ fi
+ done
+ if [ "${#smoke[@]}" -eq 0 ]; then
+ echo "::notice::No e2e smoke specs exist at the N-1 revision — nothing to run."
+ exit 0
+ fi
+ npx jest --config test/jest-e2e.json --runTestsByPath "${smoke[@]}" --ci
+
+ - name: Provenance for the executed comparison
+ if: always() && steps.prev.outputs.available == 'true'
+ run: |
+ echo "::notice::N-1 compatibility checked: N=${GITHUB_SHA} schema vs N-1=${{ steps.prev.outputs.sha }} app."
+
+ # ── Docker build verification (#131) ──────────────────────────────────────
+ # Ensures every push that changes application code doesn't silently break
+ # the Dockerfile. Image publishing to a registry can be wired up separately
+ # once registry credentials are in place.
+ docker:
+ name: Docker – build verification
+ runs-on: ubuntu-latest
+ needs: backend
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+
+ - name: Build Docker image
+ uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
+ with:
+ context: .
+ push: false
+ tags: vortex-backend:ci
+
+ # ── OpenAPI drift check (issue #318) ──────────────────────────────────────
+ # Verifies that src/generated/openapi.json (and the api-types.ts it drives)
+ # still matches what the current controllers would produce. A PR that
+ # changes a controller DTO or route without re-running `npm run generate:client`
+ # will fail here before stale types ship to SDK consumers.
+ #
+ # The job runs only on pull_request events so we don't block main pushes that
+ # are the *result* of running the generator. The sdk-publish job below
+ # re-generates on tags anyway, so production is always up-to-date.
+ openapi-drift:
+ name: OpenAPI drift check
+ runs-on: ubuntu-latest
+ needs: backend
+ if: github.event_name == 'pull_request'
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_USER: vortex
+ POSTGRES_PASSWORD: vortex
+ POSTGRES_DB: vortex
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd="pg_isready -U vortex"
+ --health-interval=10s
+ --health-timeout=5s
+ --health-retries=5
+ env:
+ DATABASE_URL: postgresql://vortex:vortex@localhost:5432/vortex?schema=public
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+
+ # ── Solver SDK (issue #446): build + publish dry-run on every PR ───────────
+ solver-sdk:
+ name: Solver SDK – build + publish dry-run
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Restore cached Prisma client
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ with:
+ path: node_modules/.prisma
+ key: prisma-${{ runner.os }}-node20-${{ hashFiles('prisma/schema.prisma', 'package-lock.json') }}
+
+ - name: Generate Prisma client
+ run: npm run db:generate
+
+ - name: Run database migrations
+ run: npm run db:migrate:prod
+
+ # Build first — generate:client requires dist/ to boot the throw-away app.
+ - name: Build
+ run: npm run build
+
+ # Re-generate the client from the current controllers into a temp directory,
+ # then diff against the checked-in src/generated/. Any difference means
+ # a controller change was not followed by `npm run generate:client`.
+ - name: Regenerate OpenAPI client
+ run: npm run generate:client
+
+ - name: Check for OpenAPI drift
+ run: |
+ if ! git diff --exit-code src/generated/; then
+ echo ""
+ echo "::error::src/generated/ is out of date. Run \`npm run generate:client\` locally,"
+ echo "::error::commit the updated files, and push again."
+ echo ""
+ git diff src/generated/
+ exit 1
+ fi
+ echo "✅ src/generated/ is in sync with the current controllers."
+ - run: npm ci
+ - run: npm run check:version -w @vortex/solver-sdk
+ - run: npm run build -w @vortex/solver-sdk
+ - run: npm publish -w @vortex/solver-sdk --dry-run
+
+ sdk-publish:
+ name: Publish generated SDK
+ runs-on: ubuntu-latest
+ needs: [backend, docker]
+ if: startsWith(github.ref, 'refs/tags/v')
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
+
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version: 20
+ registry-url: https://registry.npmjs.org
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Regenerate client SDK
+ run: npm run generate:client
+
+ - name: Verify packaged SDK installs cleanly
+ run: |
+ tmpdir=$(mktemp -d)
+ cd "$tmpdir"
+ npm init -y
+ npm install --silent "$GITHUB_WORKSPACE/src/generated"
+ npm install --silent typescript @types/node
+ cat <<'TS' > index.ts
+ import type { paths, components } from '@vortex-protocol/backend-sdk';
+
+ type IntentList = paths['/api/v1/intents']['get'];
+ type Intent = components['schemas']['Intent'];
+
+ const fallback: Intent | undefined = undefined;
+ void fallback;
+ void IntentList;
+ TS
+ npx tsc --noEmit --moduleResolution node --module commonjs --target es2020 index.ts
+
+ - name: Publish SDK to npm
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ run: |
+ version="${GITHUB_REF_NAME#v}"
+ npm --prefix src/generated version "$version" --no-git-tag-version
+ npm --prefix src/generated publish --access public
diff --git a/README.md b/README.md
index 285ee37e..0c4972d2 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@ POST /api/v1/intents/:id/cancel — user cancels
POST /api/v1/intents/quote — get best quote from solvers
GET /api/v1/solvers — solver leaderboard
GET /api/v1/solvers/:addr/stats — solver performance stats
+POST /api/v1/solvers/:addr/heartbeat — solver liveness heartbeat (keeps the solver online)
GET /api/v1/tokens — supported tokens (filter by chain)
POST /api/v1/admin/tokens — register a token (admin key, on-chain metadata check)
PATCH /api/v1/admin/tokens — update status or re-verified metadata
diff --git a/docs/adr/0007-solver-liveness-heartbeats.md b/docs/adr/0007-solver-liveness-heartbeats.md
new file mode 100644
index 00000000..2bc129c3
--- /dev/null
+++ b/docs/adr/0007-solver-liveness-heartbeats.md
@@ -0,0 +1,27 @@
+# ADR 0007: Solver liveness heartbeats and automatic offline detection
+
+- **Status**: Accepted
+- **Date**: 2026-09-30
+- **Technical Story**: #445 — heartbeat-based liveness with cross-replica state, status events, and RFQ exclusion
+
+## Context
+
+`markLive`/`markOffline` existed but nothing called them on a schedule: a solver's bot could die while the record stayed `isActive=true`, so the quote endpoint (`POST /api/v1/intents/quote`), `GET /solvers/:address/eligible-intents`, and the capability filters kept routing work to it that nobody would fill. Detection had to survive multiple backend replicas — a beat seen by replica A must count for replica B — and a replica that loses its network must not decide, on behalf of everyone, that all of its solvers are dead.
+
+## Decision
+
+Clients heartbeat at a negotiated cadence (`SOLVER_HEARTBEAT_INTERVAL_MS`, default 10 s) over the authenticated WebSocket (`{"type":"heartbeat"}`) or a signed REST `POST /api/v1/solvers/:address/heartbeat`. Each beat refreshes two views: a per-replica in-memory expiry, and a shared Redis key per solver whose TTL is the offline window (`SOLVER_HEARTBEAT_MISSES`, default 3 → 30 s). Offline is detected by a **periodic sweep** — every interval, one `EXISTS` per active solver — rather than keyspace notifications:
+
+- keyspace notifications require `notify-keyspace-events KEx` on every Redis deployment (managed services frequently disable it) and are fire-and-forget Pub/Sub: a replica whose subscriber connection is down misses those events permanently, which is exactly when it most needs to know about expiries;
+- a poll degrades predictably: one pipelined round-trip per interval, zero Redis configuration, and it can distinguish "key expired" from "cannot reach Redis" — the distinction the partition guard depends on.
+
+The sweep is two-phase: read every shared verdict first, apply transitions second. If any read reports the store unreachable, the cycle aborts with **no** transitions, and this replica's own fresh beats always win — a partitioned replica cannot mass-flip its solvers. An offline transition flips `isActive=false` (so quotes, eligible-intents, and capability predicates drop the solver), stamps `lastActiveAt`, sets an `auto-offline` flag, and broadcasts `solver_status_changed` on the WS/SSE feed; `vortex_solver_live_by_chain` and `vortex_solver_status_changes_total` publish the state to Prometheus.
+
+Auto-offline is reversible, deliberate deactivation is not: the flag lets the next heartbeat — or a fresh authentication, itself proof of life — heal the record, while `deactivate`/`deregister` clear the flag so heartbeats can never undo an operator decision. A `lastActiveAt` within one offline window (registration, reactivation, a recent fill) counts as proof of life and delays exclusion by at most one window, so a freshly registered bot gets time to connect and a solver that is demonstrably filling is never withdrawn from quotes.
+
+## Consequences
+
+- Solvers that do not beat within `interval × misses` are excluded from quotes and capability-filtered feeds within one sweep after the window. The upgrade is behavioural: bots must send heartbeats (documented in `docs/solver-onboarding.md` §3, "Heartbeats & Liveness").
+- Multi-replica deployments must set `SOLVER_HEARTBEAT_REDIS_URL` (defaults to `REDIS_URL`); with the process-local store each replica only sees its own beats.
+- Detection latency is up to `interval × misses + interval` (window plus one sweep period), not a precise 30 s.
+- Redis being unreachable delays detection (fail-safe direction) instead of causing false exclusions; clients not beating is the only way solvers mass-transition, and it does so with one `solver_status_changed` event per solver.
diff --git a/docs/runbooks/on-call.md b/docs/runbooks/on-call.md
index 2b938d2f..c6ec65cd 100644
--- a/docs/runbooks/on-call.md
+++ b/docs/runbooks/on-call.md
@@ -19,8 +19,9 @@
8. [Scenario E — Synthetic canary failing](#scenario-e--synthetic-canary-failing)
9. [Health probes](#health-probes)
10. [Scenario F — WebSocket backplane and slow consumers](#scenario-f--websocket-backplane-and-slow-consumers)
-11. [Key configuration](#key-configuration)
-12. [Escalation path](#escalation-path)
+11. [Scenario G — Solvers mass-going offline](#scenario-g--solvers-mass-going-offline-issue-445)
+12. [Key configuration](#key-configuration)
+13. [Escalation path](#escalation-path)
---
@@ -48,6 +49,8 @@ read endpoints but does **not** take down the intent relay or WebSocket feed.
| `vortex_sweeper_sweep_duration_ms` p99 | < 50 ms under normal load |
| `vortex_sweeper_expired_total` | Monotonically increasing; spikes expected near intent `deadline` clusters |
| WS subscriber count | Stable or slowly growing; sudden drops indicate client-side churn |
+| `vortex_solver_live_by_chain{chain=...}` | Matches the connected, beating solvers per chain; a sudden drop to 0 = heartbeat problem (Scenario G) |
+| `vortex_solver_status_changes_total{status="offline"}` | Flat between deploys; a spike means mass offline detection fired (Scenario G) |
| Node.js heap | Steady-state < 200 MB; no sustained upward trend between GC cycles |
---
@@ -515,6 +518,39 @@ handlers never wait for Redis.
---
+## Scenario G — Solvers mass-going offline (issue #445)
+
+**Symptoms:** `vortex_solver_live_by_chain` drops toward 0 while the bots
+look healthy; `vortex_solver_status_changes_total{status="offline"}` spikes;
+`POST /api/v1/intents/quote` returns few or no solvers; log lines
+`liveness sweep aborted: shared store unreachable (partition guard, no
+transitions applied)`.
+
+1. **Check the shared liveness store.**
+ `redis-cli -u "$SOLVER_HEARTBEAT_REDIS_URL" ping` (falls back to
+ `REDIS_URL`; when both are empty each replica only sees its own
+ heartbeats — with more than one replica that is a misconfiguration).
+ Sweep aborts mean the partition guard is holding: no transitions are
+ applied while Redis is unreachable, so a Redis outage alone should not
+ mass-flip solvers. Mass flips with a healthy Redis point at the clients
+ themselves not beating (bad bot deploy, or a cadence slower than
+ `SOLVER_HEARTBEAT_INTERVAL_MS × SOLVER_HEARTBEAT_MISSES`).
+2. **Check the cadence.** Bots must send `{ "type": "heartbeat" }` (or a
+ signed `POST /api/v1/solvers/:addr/heartbeat`) at the interval returned
+ in `auth_ok.heartbeatIntervalMs` — default 10 000 ms, offline after
+ 3 misses (30 s). Verify the bot actually received `heartbeat_ack`
+ `accepted: true` — an unauthenticated connection is rejected and never
+ refreshes liveness.
+3. **Check clocks.** The local view uses each replica's `Date.now()`, the
+ shared view uses Redis TTLs; a large NTP step can shift detection by one
+ cycle (10 s) but not cause a permanent outage.
+4. **Recovery is automatic.** The next heartbeat — or a fresh
+ authentication — re-activates an auto-offlined solver and emits
+ `solver_status_changed{status="online"}`. Records deactivated with
+ `deactivate`/`deregister` intentionally stay offline until `reactivate`.
+
+---
+
## Key configuration
| Variable | Default | Effect |
@@ -532,6 +568,9 @@ handlers never wait for Redis.
| `ADMIN_API_KEYS` | empty (admin APIs disabled) | `id:role:secret` entries for admin / superadmin endpoints |
| `PROCESS_ROLE` / `JOBS_DRIVER` | `all` / `memory` | Where job workers run; `bullmq` for multi-instance |
| `CANARY_ADDRESSES` | empty | Canary accounts excluded from public stats |
+| `SOLVER_HEARTBEAT_INTERVAL_MS` | `10000` | Cadence clients are told to beat at; also the sweep period |
+| `SOLVER_HEARTBEAT_MISSES` | `3` | Misses tolerated before auto-offline (window = interval × misses) |
+| `SOLVER_HEARTBEAT_REDIS_URL` | `REDIS_URL` when set | Shared liveness store; empty = process-local (single replica) |
---
diff --git a/docs/solver-onboarding.md b/docs/solver-onboarding.md
index f03bfe01..ac07c205 100644
--- a/docs/solver-onboarding.md
+++ b/docs/solver-onboarding.md
@@ -216,6 +216,68 @@ On connection or reconnection, the bot can request event replay from its last re
}
```
+### Heartbeats & Liveness
+
+The backend tracks whether each solver process is actually running (issue
+#445). The negotiated cadence is advertised in the `connected` frame and in
+`auth_ok`:
+
+```json
+{ "type": "auth_ok", "method": "signature", "heartbeatIntervalMs": 10000 }
+```
+
+Send a heartbeat at that interval (default every 10 s):
+
+```json
+{ "type": "heartbeat" }
+```
+
+The server acknowledges with the cadence again:
+
+```json
+{ "type": "heartbeat_ack", "accepted": true, "heartbeatIntervalMs": 10000 }
+```
+
+Unauthenticated connections receive `accepted: false` — complete the `auth`
+step first. After `heartbeatIntervalMs × misses` without a beat (default
+10 000 × 3 = 30 s) the solver is marked **offline** and every connected
+subscriber receives:
+
+```json
+{
+ "type": "solver_status_changed",
+ "solver": "G...",
+ "status": "offline",
+ "lastActiveAt": 1759248000,
+ "reason": "missed_heartbeats",
+ "at": 1759248030
+}
+```
+
+While offline the solver is excluded from `POST /api/v1/intents/quote`
+results, stops receiving capability-filtered intent events, and is rejected
+on new WS authentications. The next heartbeat — or simply re-authenticating
+on a fresh connection — flips it back to `online` (the event is re-emitted
+with `reason: "heartbeat"`). Deliberate deactivations (`deactivate` /
+`deregister`) are **not** undone by heartbeats; they require `reactivate`.
+
+Clients without a persistent connection can beat over REST instead. The
+timestamp must be fresh (within `max(60 s, offline window)`), signed over
+the canonical message `heartbeat:
:`:
+
+```bash
+MSG="heartbeat:$SOLVER_ADDRESS:$TS"
+SIG=$(sign "$MSG") # Ed25519 over the canonical message
+curl -X POST "$BACKEND_URL/api/v1/solvers/$SOLVER_ADDRESS/heartbeat" \
+ -H "Content-Type: application/json" \
+ -d "{\"timestamp\": $TS, \"signature\": \"$SIG\"}"
+# -> { "status": "online", "lastActiveAt": ..., "heartbeatIntervalMs": 10000 }
+```
+
+Multi-replica deployments must share liveness
+(`SOLVER_HEARTBEAT_REDIS_URL`, which defaults to `REDIS_URL`) — otherwise
+each replica only sees the heartbeats routed to it.
+
### Accepting & Filling Intents with Auth Signatures
When a solver sees an open intent, it performs 2 steps:
diff --git a/jest.config.js b/jest.config.js
index 5c7b8f21..00bedd55 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -28,6 +28,7 @@ module.exports = {
collectCoverageFrom: ["**/*.(t|j)s"],
moduleNameMapper: {
"^@nestjs/schedule$": "/../test/__mocks__/nestjs-schedule.ts",
+ "^@nestjs/event-emitter$": "/../test/__mocks__/nestjs-event-emitter.ts",
},
},
@@ -48,6 +49,24 @@ module.exports = {
],
},
},
+
+ // ── Simulator tooling suite (issue #452) ───────────────────────────────
+ // The replay harness lives in tools/simulator/ (outside src/); same
+ // broader-rootDir trick as the scripts suite.
+ {
+ displayName: "tools",
+ testEnvironment: "node",
+ rootDir: ".",
+ testMatch: ["/tools/**/*.spec.ts"],
+ transform: {
+ "^.+\\.tsx?$": [
+ "ts-jest",
+ {
+ tsconfig: "./tsconfig.tools.json",
+ },
+ ],
+ },
+ },
],
// Coverage is collected from the project-level collectCoverageFrom above.
diff --git a/package-lock.json b/package-lock.json
index 3816fce3..c5682917 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"packages/*"
],
"dependencies": {
+ "@aws-sdk/client-s3": "^3.654.0",
"@bull-board/api": "^9.10.1",
"@bull-board/express": "^9.10.1",
"@msgpack/msgpack": "^3.0.0",
@@ -59,14 +60,14 @@
"@commitlint/config-conventional": "^19.8.1",
"@msgpack/msgpack": "^3.0.0",
"@nestjs/cli": "^11.0.24",
- "@nestjs/schematics": "^12.0.5",
+ "@nestjs/schematics": "^11.1.0",
"@nestjs/testing": "^11.1.28",
"@stryker-mutator/core": "^8.0.0",
"@stryker-mutator/jest-runner": "^8.0.0",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.25",
"@types/jest": "^30.0.0",
- "@types/node": "^20.14.2",
+ "@types/node": "^26.6.3",
"@types/parquetjs": "^0.10.6",
"@types/supertest": "^7.2.0",
"@types/uuid": "^10.0.0",
@@ -76,7 +77,7 @@
"eslint": "^8.57.0",
"fast-check": "^4.10.2",
"husky": "^9.1.7",
- "jest": "^30.5.2",
+ "jest": "^30.4.2",
"openapi-typescript": "^7.8.0",
"prisma": "5.22.0",
"supertest": "^7.2.2",
@@ -280,6 +281,314 @@
"module-details-from-path": "^1.0.4"
}
},
+ "node_modules/@aws-sdk/checksums": {
+ "version": "3.1001.1",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.1.tgz",
+ "integrity": "sha512-x12Q17KYlJAd3nKf8LV5LV0vt8sh8/6YfQLGPtrGnQf/tW4jqxPGq5GPpuVitpQYM3eUR4XB7CbxZf751NMbLw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-s3": {
+ "version": "3.1145.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1145.0.tgz",
+ "integrity": "sha512-MPKAQdx8qCZW1/ChDPILNehtAn/BMP4GNTnaH/xhGCWg3v50ADVq1K2WzxIrFfeANdWUyjwiY0CXDBhclsNWgQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/checksums": "^3.1001.1",
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/credential-provider-node": "^3.972.84",
+ "@aws-sdk/middleware-sdk-s3": "^3.972.77",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.47",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/fetch-http-handler": "^5.8.0",
+ "@smithy/node-http-handler": "^4.12.1",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/core": {
+ "version": "3.978.1",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz",
+ "integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.974.6",
+ "@aws-sdk/xml-builder": "^3.972.41",
+ "@aws/lambda-invoke-store": "^0.3.0",
+ "@smithy/core": "^3.35.0",
+ "@smithy/signature-v4": "^5.7.3",
+ "@smithy/types": "^4.19.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.972.72",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz",
+ "integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.972.74",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz",
+ "integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/fetch-http-handler": "^5.8.0",
+ "@smithy/node-http-handler": "^4.12.1",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.973.17",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz",
+ "integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/credential-provider-env": "^3.972.72",
+ "@aws-sdk/credential-provider-http": "^3.972.74",
+ "@aws-sdk/credential-provider-login": "^3.972.79",
+ "@aws-sdk/credential-provider-process": "^3.972.72",
+ "@aws-sdk/credential-provider-sso": "^3.973.16",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.78",
+ "@aws-sdk/nested-clients": "^3.997.46",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/credential-provider-imds": "^4.5.2",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-login": {
+ "version": "3.972.79",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz",
+ "integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/nested-clients": "^3.997.46",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.972.84",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz",
+ "integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "^3.972.72",
+ "@aws-sdk/credential-provider-http": "^3.972.74",
+ "@aws-sdk/credential-provider-ini": "^3.973.17",
+ "@aws-sdk/credential-provider-process": "^3.972.72",
+ "@aws-sdk/credential-provider-sso": "^3.973.16",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.78",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/credential-provider-imds": "^4.5.2",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.972.72",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz",
+ "integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.973.16",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz",
+ "integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/nested-clients": "^3.997.46",
+ "@aws-sdk/token-providers": "3.1138.0",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.972.78",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz",
+ "integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/nested-clients": "^3.997.46",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-sdk-s3": {
+ "version": "3.972.77",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.77.tgz",
+ "integrity": "sha512-E7W2UOeUoc+lg3uIfR/dM7ZwusHwhBQrKMnlkRv4EXRR+C0YtV1pg25xC7GdZIhXH+NAMgZPCbE7o5to2cjFiw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.47",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients": {
+ "version": "3.997.46",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz",
+ "integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.47",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/fetch-http-handler": "^5.8.0",
+ "@smithy/node-http-handler": "^4.12.1",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.996.47",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz",
+ "integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/signature-v4": "^5.7.3",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/token-providers": {
+ "version": "3.1138.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz",
+ "integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/nested-clients": "^3.997.46",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/types": {
+ "version": "3.974.6",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz",
+ "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/xml-builder": {
+ "version": "3.972.41",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz",
+ "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws/lambda-invoke-store": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
+ "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -987,8 +1296,7 @@
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/@borewit/text-codec": {
"version": "0.2.2",
@@ -1544,7 +1852,6 @@
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
@@ -1556,7 +1863,6 @@
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
@@ -1567,7 +1873,6 @@
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
@@ -2678,7 +2983,6 @@
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
"integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
"dev": true,
- "license": "ISC",
"dependencies": {
"camelcase": "^5.3.1",
"find-up": "^4.1.0",
@@ -2695,7 +2999,6 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
@@ -2705,7 +3008,6 @@
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
@@ -2715,11 +3017,10 @@
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
- "version": "3.15.2",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz",
- "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==",
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
+ "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==",
"dev": true,
- "license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@@ -2733,7 +3034,6 @@
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
@@ -2746,7 +3046,6 @@
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
- "license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
@@ -2762,7 +3061,6 @@
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
@@ -2775,7 +3073,6 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -2790,17 +3087,16 @@
}
},
"node_modules/@jest/console": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.2.tgz",
- "integrity": "sha512-e8sQC4pCMHPBrPX7Hk8TJr+DM5dkUnImSTNag8SEWFAFAF6xIz4AJvCKapnrdHnZxep4bxw4PhG+ZbimYBMyKw==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz",
+ "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"chalk": "^4.1.2",
- "jest-message-util": "30.5.1",
- "jest-util": "30.5.1",
+ "jest-message-util": "30.4.1",
+ "jest-util": "30.4.1",
"slash": "^3.0.0"
},
"engines": {
@@ -2808,18 +3104,17 @@
}
},
"node_modules/@jest/core": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.2.tgz",
- "integrity": "sha512-/jqvFWoJF7LV1a6AUpYMbMm+2yIYsHJfR474H0SCzr9k8tnO/jl9wRg6zSG6lxgf6EozNlCG4amFzWsbZSvvZA==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz",
+ "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/console": "30.5.2",
- "@jest/pattern": "30.5.0",
- "@jest/reporters": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/transform": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/console": "30.4.1",
+ "@jest/pattern": "30.4.0",
+ "@jest/reporters": "30.4.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/transform": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"ansi-escapes": "^4.3.2",
"chalk": "^4.1.2",
@@ -2827,20 +3122,20 @@
"exit-x": "^0.2.2",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
- "jest-changed-files": "30.5.1",
- "jest-config": "30.5.2",
- "jest-haste-map": "30.5.1",
- "jest-message-util": "30.5.1",
- "jest-regex-util": "30.5.0",
- "jest-resolve": "30.5.1",
- "jest-resolve-dependencies": "30.5.2",
- "jest-runner": "30.5.2",
- "jest-runtime": "30.5.2",
- "jest-snapshot": "30.5.2",
- "jest-util": "30.5.1",
- "jest-validate": "30.5.1",
- "jest-watcher": "30.5.2",
- "pretty-format": "30.5.1",
+ "jest-changed-files": "30.4.1",
+ "jest-config": "30.4.2",
+ "jest-haste-map": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-regex-util": "30.4.0",
+ "jest-resolve": "30.4.1",
+ "jest-resolve-dependencies": "30.4.2",
+ "jest-runner": "30.4.2",
+ "jest-runtime": "30.4.2",
+ "jest-snapshot": "30.4.1",
+ "jest-util": "30.4.1",
+ "jest-validate": "30.4.1",
+ "jest-watcher": "30.4.1",
+ "pretty-format": "30.4.1",
"slash": "^3.0.0"
},
"engines": {
@@ -2856,159 +3151,134 @@
}
},
"node_modules/@jest/diff-sequences": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz",
- "integrity": "sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz",
+ "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==",
"dev": true,
- "license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/environment": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.2.tgz",
- "integrity": "sha512-nwFPOUMbmsjNNRCpnJpB9HbSZu4C07wDDds++5j+j1MezzPLxMPDdbdUVjr2o4RULDOValnDd70taPFI1EtxAg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz",
+ "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/fake-timers": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/fake-timers": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
- "jest-mock": "30.5.2"
+ "jest-mock": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/expect": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.5.2.tgz",
- "integrity": "sha512-2E+1Pg6jJxbRjLNLpciC71iri/3qsKJL222/aXJEFwC0VdUSvfS+JZQQLEVjUYYc8t8pQu2MGE3cF2+bNTYRsg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz",
+ "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "expect": "30.5.2",
- "jest-snapshot": "30.5.2"
+ "expect": "30.4.1",
+ "jest-snapshot": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/expect-utils": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.2.tgz",
- "integrity": "sha512-d/HOLSrJUlBIl6n3LzOa7cjW3xdePm5H5gQw2lixZ/0h157l9sw3mLblzFqnPcTbhDzhRQdBOX3A3KcG73nm5Q==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz",
+ "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/get-type": "30.5.0"
+ "@jest/get-type": "30.1.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/fake-timers": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.2.tgz",
- "integrity": "sha512-GKk0h0tKT5SpDPxDhPg3r9mm5s8/YCBzVNMOGdRiQXwUiMsjU10mVPlMaQstH6/cu50vOb8N9oQUMETHuwwxKw==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz",
+ "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"@sinonjs/fake-timers": "^15.4.0",
"@types/node": "*",
- "jest-message-util": "30.5.1",
- "jest-mock": "30.5.2",
- "jest-util": "30.5.1"
+ "jest-message-util": "30.4.1",
+ "jest-mock": "30.4.1",
+ "jest-util": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/get-type": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.5.0.tgz",
- "integrity": "sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==",
+ "version": "30.1.0",
+ "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz",
+ "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/globals": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.5.2.tgz",
- "integrity": "sha512-62zfS+dL+M5aTNHXCNLyqD+j4oqxTIzDYrZbpqMP8Yn+gvlAGyzC6BSPNb1ZmFOakV1RtFYX/O3FCQXSWmQdgg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz",
+ "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/environment": "30.5.2",
- "@jest/expect": "30.5.2",
- "@jest/types": "30.5.1",
- "jest-mock": "30.5.2"
+ "@jest/environment": "30.4.1",
+ "@jest/expect": "30.4.1",
+ "@jest/types": "30.4.1",
+ "jest-mock": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/pattern": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.5.0.tgz",
- "integrity": "sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz",
+ "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/node": "*",
- "jest-regex-util": "30.5.0"
+ "jest-regex-util": "30.4.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
- "node_modules/@jest/react-is-18": {
- "name": "react-is",
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
- "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jest/react-is-19": {
- "name": "react-is",
- "version": "19.3.0",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.0.tgz",
- "integrity": "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@jest/reporters": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.5.2.tgz",
- "integrity": "sha512-ev6E9iG73twQcfAfoTnviuYkr3yOrYbzAtMFCimv2fxHXPIp9PBt2u1wNMt0aYWgl8FPwIcS8oqRJg6alnht4Q==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz",
+ "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^0.2.3",
- "@jest/console": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/transform": "30.5.2",
- "@jest/types": "30.5.1",
- "@jridgewell/trace-mapping": "^0.3.31",
+ "@jest/console": "30.4.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/transform": "30.4.1",
+ "@jest/types": "30.4.1",
+ "@jridgewell/trace-mapping": "^0.3.25",
"@types/node": "*",
"chalk": "^4.1.2",
"collect-v8-coverage": "^1.0.2",
"exit-x": "^0.2.2",
- "glob": "^13.0.6",
+ "glob": "^10.5.0",
"graceful-fs": "^4.2.11",
"istanbul-lib-coverage": "^3.0.0",
"istanbul-lib-instrument": "^6.0.0",
"istanbul-lib-report": "^3.0.0",
"istanbul-lib-source-maps": "^5.0.0",
"istanbul-reports": "^3.1.3",
- "jest-message-util": "30.5.1",
- "jest-util": "30.5.1",
- "jest-worker": "30.5.1",
+ "jest-message-util": "30.4.1",
+ "jest-util": "30.4.1",
+ "jest-worker": "30.4.1",
"slash": "^3.0.0",
"string-length": "^4.0.2",
"v8-to-istanbul": "^9.0.1"
@@ -3025,12 +3295,54 @@
}
}
},
+ "node_modules/@jest/reporters/node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "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",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
+ "node_modules/@jest/reporters/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/@jest/schemas": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz",
- "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz",
+ "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
},
@@ -3039,13 +3351,12 @@
}
},
"node_modules/@jest/snapshot-utils": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.5.1.tgz",
- "integrity": "sha512-V3wnxNtiVmw5PPVg433Cn3VdXnsOeu/ofLw3KC04Bn2y1wIlU5kozXQn48rhrgj/02LrCVtntTEF1yBha0XSUw==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz",
+ "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"natural-compare": "^1.4.0"
@@ -3055,15 +3366,13 @@
}
},
"node_modules/@jest/source-map": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.5.2.tgz",
- "integrity": "sha512-c5CehvkbfHX6yyNg7bMn2ylqlp9AHXuZ/3hPH/Lh5bxD/vefd/lV9HBjMqXef9yQNqkdwyZvkLSVSwB38hy8Qw==",
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz",
+ "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jridgewell/trace-mapping": "^0.3.31",
+ "@jridgewell/trace-mapping": "^0.3.25",
"callsites": "^3.1.0",
- "convert-source-map": "^2.0.0",
"graceful-fs": "^4.2.11"
},
"engines": {
@@ -3071,14 +3380,13 @@
}
},
"node_modules/@jest/test-result": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.5.2.tgz",
- "integrity": "sha512-eO6YU5rsLfs60ne694e0IAmRgeBnoA8mu0nlxXDbl/1j5km0o2vO9/VbbUIKIH+WRKKEI+ELgwraRGn2Lhep8g==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz",
+ "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/console": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/console": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/istanbul-lib-coverage": "^2.0.6",
"collect-v8-coverage": "^1.0.2"
},
@@ -3087,15 +3395,14 @@
}
},
"node_modules/@jest/test-sequencer": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.5.2.tgz",
- "integrity": "sha512-bmij8jZyYAC47ExT2GeP7PcrlwSNS+Bp3KeRuBH88gpcxqDv6fljN8PemS4J/lLZ79xb6mpaennQTTq0zUAVVg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz",
+ "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/test-result": "30.5.2",
+ "@jest/test-result": "30.4.1",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.5.1",
+ "jest-haste-map": "30.4.1",
"slash": "^3.0.0"
},
"engines": {
@@ -3103,23 +3410,22 @@
}
},
"node_modules/@jest/transform": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.5.2.tgz",
- "integrity": "sha512-LBGwmaE886C41jDZ65eqR1YbXM7642MK3ZhTowZd5+xDlrDLuf4ePuDdDmVSvcr8NTVJ3MqxLkMbUCzuigUaoQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz",
+ "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
- "@jest/types": "30.5.1",
- "@jridgewell/trace-mapping": "^0.3.31",
- "babel-plugin-istanbul": "^8.0.0",
+ "@jest/types": "30.4.1",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "babel-plugin-istanbul": "^7.0.1",
"chalk": "^4.1.2",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.5.1",
- "jest-regex-util": "30.5.0",
- "jest-util": "30.5.1",
+ "jest-haste-map": "30.4.1",
+ "jest-regex-util": "30.4.0",
+ "jest-util": "30.4.1",
"pirates": "^4.0.7",
"slash": "^3.0.0",
"write-file-atomic": "^5.0.1"
@@ -3129,14 +3435,13 @@
}
},
"node_modules/@jest/types": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.1.tgz",
- "integrity": "sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz",
+ "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/pattern": "30.5.0",
- "@jest/schemas": "30.5.0",
+ "@jest/pattern": "30.4.0",
+ "@jest/schemas": "30.4.1",
"@types/istanbul-lib-coverage": "^2.0.6",
"@types/istanbul-reports": "^3.0.4",
"@types/node": "*",
@@ -3313,25 +3618,21 @@
]
},
"node_modules/@napi-rs/wasm-runtime": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz",
- "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==",
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.3"
},
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
- },
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
- "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4",
- "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4"
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
}
},
"node_modules/@nestjs/cli": {
@@ -3378,96 +3679,16 @@
}
}
},
- "node_modules/@nestjs/cli/node_modules/@nestjs/schematics": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz",
- "integrity": "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==",
- "dev": true,
- "license": "MIT",
+ "node_modules/@nestjs/common": {
+ "version": "11.1.28",
+ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.28.tgz",
+ "integrity": "sha512-bRImsxibie+AM7xjdwcrm/gr5YeacI65kSBNzTufa1Ib5iwziaY/lqMtRh9THq6pbV4e1HP9aI2ZxGUumnmaoQ==",
"dependencies": {
- "@angular-devkit/core": "19.2.24",
- "@angular-devkit/schematics": "19.2.24",
- "comment-json": "5.0.0",
- "jsonc-parser": "3.3.1",
- "pluralize": "8.0.0"
- },
- "peerDependencies": {
- "prettier": "^3.0.0",
- "typescript": ">=4.8.2"
- },
- "peerDependenciesMeta": {
- "prettier": {
- "optional": true
- }
- }
- },
- "node_modules/@nestjs/cli/node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": {
- "version": "19.2.24",
- "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz",
- "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ajv": "8.18.0",
- "ajv-formats": "3.0.1",
- "jsonc-parser": "3.3.1",
- "picomatch": "4.0.4",
- "rxjs": "7.8.1",
- "source-map": "0.7.4"
- },
- "engines": {
- "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- },
- "peerDependencies": {
- "chokidar": "^4.0.0"
- },
- "peerDependenciesMeta": {
- "chokidar": {
- "optional": true
- }
- }
- },
- "node_modules/@nestjs/cli/node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": {
- "version": "19.2.24",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz",
- "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@angular-devkit/core": "19.2.24",
- "jsonc-parser": "3.3.1",
- "magic-string": "0.30.17",
- "ora": "5.4.1",
- "rxjs": "7.8.1"
- },
- "engines": {
- "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
- "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
- "yarn": ">= 1.13.0"
- }
- },
- "node_modules/@nestjs/cli/node_modules/rxjs": {
- "version": "7.8.1",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
- "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.1.0"
- }
- },
- "node_modules/@nestjs/common": {
- "version": "11.1.28",
- "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.28.tgz",
- "integrity": "sha512-bRImsxibie+AM7xjdwcrm/gr5YeacI65kSBNzTufa1Ib5iwziaY/lqMtRh9THq6pbV4e1HP9aI2ZxGUumnmaoQ==",
- "dependencies": {
- "file-type": "21.3.4",
- "iterare": "1.2.1",
- "load-esm": "1.0.3",
- "tslib": "2.8.1",
- "uid": "2.0.2"
+ "file-type": "21.3.4",
+ "iterare": "1.2.1",
+ "load-esm": "1.0.3",
+ "tslib": "2.8.1",
+ "uid": "2.0.2"
},
"funding": {
"type": "opencollective",
@@ -3661,40 +3882,20 @@
}
},
"node_modules/@nestjs/schematics": {
- "version": "12.0.5",
- "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-12.0.5.tgz",
- "integrity": "sha512-AuZJVAglqzjFyXVy1EIyzjyEArw6mdOF19vWTgVviJICvVAK8DT97q+6CX4v3wWF",
- "dev": true,
- "dependencies": {
- "cron": "4.4.0"
- },
- "engines": {
- "node": ">=20.19.0"
- },
- "peerDependencies": {
- "@nestjs/common": "^11.0.0 || ^12.0.0",
- "@nestjs/core": "^11.0.0 || ^12.0.0"
- }
- },
- "node_modules/@nestjs/schematics": {
- "version": "12.0.5",
- "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-12.0.5.tgz",
- "integrity": "sha512-AuZJVAglqzjFyXVy1EIyzjyEArw6mdOF19vWTgVviJICvVAK8DT97q+6CX4v3wWFhGZoDbGiDnbq6EsGub7/bA==",
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz",
+ "integrity": "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@angular-devkit/core": "22.1.8",
- "@angular-devkit/schematics": "22.1.8",
+ "@angular-devkit/core": "19.2.24",
+ "@angular-devkit/schematics": "19.2.24",
"comment-json": "5.0.0",
"jsonc-parser": "3.3.1",
"pluralize": "8.0.0"
},
- "engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
- },
"peerDependencies": {
"prettier": "^3.0.0",
- "typescript": ">=6.0.0"
+ "typescript": ">=4.8.2"
},
"peerDependenciesMeta": {
"prettier": {
@@ -3703,26 +3904,25 @@
}
},
"node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": {
- "version": "22.1.8",
- "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.1.8.tgz",
- "integrity": "sha512-34lsgg2FwMVBX7nR/ZqzRUcdvdYPvSB8XAEr2GudXAyZwLI9ehzfQlagAWR/gEb8p4uCFZW0r7uU0toxydq4Rw==",
+ "version": "19.2.24",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz",
+ "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "ajv": "8.20.0",
+ "ajv": "8.18.0",
"ajv-formats": "3.0.1",
"jsonc-parser": "3.3.1",
- "picomatch": "4.0.5",
- "rxjs": "7.8.2",
- "source-map": "0.7.6"
+ "picomatch": "4.0.4",
+ "rxjs": "7.8.1",
+ "source-map": "0.7.4"
},
"engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0",
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
"yarn": ">= 1.13.0"
},
"peerDependencies": {
- "chokidar": "^5.0.0"
+ "chokidar": "^4.0.0"
},
"peerDependenciesMeta": {
"chokidar": {
@@ -3731,259 +3931,30 @@
}
},
"node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": {
- "version": "22.1.8",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.1.8.tgz",
- "integrity": "sha512-Pv3cPa/44kvEwYcqwx4Ns5dhIDmcFNSHhbpheqiEG1Z/u4e2t4zHKDtE3eHZB+8o+IcC7xJj+d+AqGR44RoZDA==",
+ "version": "19.2.24",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz",
+ "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@angular-devkit/core": "22.1.8",
+ "@angular-devkit/core": "19.2.24",
"jsonc-parser": "3.3.1",
- "magic-string": "1.0.0",
- "ora": "9.4.1",
- "rxjs": "7.8.2"
+ "magic-string": "0.30.17",
+ "ora": "5.4.1",
+ "rxjs": "7.8.1"
},
"engines": {
- "node": "^22.22.3 || ^24.15.0 || >=26.0.0",
+ "node": "^18.19.1 || ^20.11.1 || >=22.0.0",
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
"yarn": ">= 1.13.0"
}
},
- "node_modules/@nestjs/schematics/node_modules/ajv": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
- "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/ansi-regex": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz",
- "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/chalk": {
- "version": "5.6.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
- "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.17.0 || ^14.13 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/cli-cursor": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
- "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "restore-cursor": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/cli-spinners": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
- "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18.20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/is-interactive": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
- "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/is-unicode-supported": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
- "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/log-symbols": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
- "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-unicode-supported": "^2.0.0",
- "yoctocolors": "^2.1.1"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/magic-string": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.0.0.tgz",
- "integrity": "sha512-CGvjzMN08iv6w1mm4/x3Gh1hLb4VnyRUA15FFpl6CsCIGGoe36k7kY5KNz9QDbSBN5I/fWHM6ZlIkUTa5xdUEA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/onetime": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
- "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mimic-function": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/ora": {
- "version": "9.4.1",
- "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz",
- "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^5.6.2",
- "cli-cursor": "^5.0.0",
- "cli-spinners": "^3.2.0",
- "is-interactive": "^2.0.0",
- "is-unicode-supported": "^2.1.0",
- "log-symbols": "^7.0.1",
- "stdin-discarder": "^0.3.2",
- "string-width": "^8.1.0"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/restore-cursor": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
- "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "onetime": "^7.0.0",
- "signal-exit": "^4.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/source-map": {
- "version": "0.7.6",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
- "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/string-width": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz",
- "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.5.0",
- "strip-ansi": "^7.1.2"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@nestjs/schematics/node_modules/strip-ansi": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
- "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "node_modules/@nestjs/schematics/node_modules/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "ansi-regex": "^6.2.2"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ "tslib": "^2.1.0"
}
},
"node_modules/@nestjs/swagger": {
@@ -5461,400 +5432,95 @@
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/sdk-trace": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz",
- "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.10.0",
- "@opentelemetry/resources": "2.10.0",
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/sdk-trace-base": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz",
- "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.10.0",
- "@opentelemetry/resources": "2.10.0",
- "@opentelemetry/sdk-trace": "2.10.0",
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/sdk-trace-node": {
- "version": "2.10.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.10.0.tgz",
- "integrity": "sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/context-async-hooks": "2.10.0",
- "@opentelemetry/core": "2.10.0",
- "@opentelemetry/sdk-trace-base": "2.10.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.43.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
- "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@opentelemetry/sql-common": {
- "version": "0.42.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.42.0.tgz",
- "integrity": "sha512-nwUwUU+8O8a4bnLqk6CodWeegGMEANgC94KTAhXcpGWLrW/2/hek/0ajNbjXnSOoNuCX+nteUPs46HFHhou9Xw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.1.0"
- }
- },
- "node_modules/@paralleldrive/cuid2": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
- "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
- "dev": true,
- "dependencies": {
- "@noble/hashes": "^1.1.5"
- }
- },
- "node_modules/@parcel/watcher": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz",
- "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "dependencies": {
- "detect-libc": "^2.0.3",
- "is-glob": "^4.0.3",
- "node-addon-api": "^7.0.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "@parcel/watcher-android-arm64": "2.6.0",
- "@parcel/watcher-darwin-arm64": "2.6.0",
- "@parcel/watcher-darwin-x64": "2.6.0",
- "@parcel/watcher-freebsd-x64": "2.6.0",
- "@parcel/watcher-linux-arm-glibc": "2.6.0",
- "@parcel/watcher-linux-arm-musl": "2.6.0",
- "@parcel/watcher-linux-arm64-glibc": "2.6.0",
- "@parcel/watcher-linux-arm64-musl": "2.6.0",
- "@parcel/watcher-linux-x64-glibc": "2.6.0",
- "@parcel/watcher-linux-x64-musl": "2.6.0",
- "@parcel/watcher-win32-arm64": "2.6.0",
- "@parcel/watcher-win32-x64": "2.6.0"
- }
- },
- "node_modules/@parcel/watcher-android-arm64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz",
- "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-darwin-arm64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz",
- "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-darwin-x64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz",
- "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-freebsd-x64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz",
- "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm-glibc": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz",
- "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm-musl": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz",
- "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/@parcel/watcher-linux-arm64-glibc": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz",
- "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10.0.0"
+ "node": "^18.19.0 || >=20.6.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
- "node_modules/@parcel/watcher-linux-arm64-musl": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz",
- "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
+ "node_modules/@opentelemetry/sdk-trace": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz",
+ "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.10.0",
+ "@opentelemetry/resources": "2.10.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
"engines": {
- "node": ">= 10.0.0"
+ "node": "^18.19.0 || >=20.6.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
- "node_modules/@parcel/watcher-linux-x64-glibc": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz",
- "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz",
+ "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.10.0",
+ "@opentelemetry/resources": "2.10.0",
+ "@opentelemetry/sdk-trace": "2.10.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
"engines": {
- "node": ">= 10.0.0"
+ "node": "^18.19.0 || >=20.6.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
- "node_modules/@parcel/watcher-linux-x64-musl": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz",
- "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
+ "node_modules/@opentelemetry/sdk-trace-node": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.10.0.tgz",
+ "integrity": "sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/context-async-hooks": "2.10.0",
+ "@opentelemetry/core": "2.10.0",
+ "@opentelemetry/sdk-trace-base": "2.10.0"
+ },
"engines": {
- "node": ">= 10.0.0"
+ "node": "^18.19.0 || >=20.6.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
- "node_modules/@parcel/watcher-win32-arm64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz",
- "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
+ "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
+ "license": "Apache-2.0",
"engines": {
- "node": ">= 10.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "node": ">=14"
}
},
- "node_modules/@parcel/watcher-win32-x64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz",
- "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
+ "node_modules/@opentelemetry/sql-common": {
+ "version": "0.42.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.42.0.tgz",
+ "integrity": "sha512-nwUwUU+8O8a4bnLqk6CodWeegGMEANgC94KTAhXcpGWLrW/2/hek/0ajNbjXnSOoNuCX+nteUPs46HFHhou9Xw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "^2.0.0"
+ },
"engines": {
- "node": ">= 10.0.0"
+ "node": "^18.19.0 || >=20.6.0"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0"
+ }
+ },
+ "node_modules/@paralleldrive/cuid2": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
+ "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
+ "dev": true,
+ "dependencies": {
+ "@noble/hashes": "^1.1.5"
}
},
"node_modules/@pkgjs/parseargs": {
@@ -5871,7 +5537,6 @@
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz",
"integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
@@ -6306,8 +5971,7 @@
"version": "0.34.52",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz",
"integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/@sindresorhus/merge-streams": {
"version": "4.0.0",
@@ -6327,7 +5991,6 @@
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
"integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
"dev": true,
- "license": "BSD-3-Clause",
"dependencies": {
"type-detect": "4.0.8"
}
@@ -6337,11 +6000,91 @@
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz",
"integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==",
"dev": true,
- "license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1"
}
},
+ "node_modules/@smithy/core": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.1.tgz",
+ "integrity": "sha512-i4YPS4B6ts7bjn7UwLnGjiZdprOvHvgGobFZsYK3GIY3E5hIqtj0rReU69BcTpGp+fvtraSNXeG1l+jtJvF55w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/credential-provider-imds": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz",
+ "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.33.2",
+ "@smithy/types": "^4.17.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/fetch-http-handler": {
+ "version": "5.8.0",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz",
+ "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.33.3",
+ "@smithy/types": "^4.18.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/node-http-handler": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz",
+ "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.33.3",
+ "@smithy/types": "^4.18.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/signature-v4": {
+ "version": "5.7.4",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz",
+ "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/types": {
+ "version": "4.19.0",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz",
+ "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@so-ric/colorspace": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
@@ -7027,11 +6770,10 @@
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
},
"node_modules/@tybys/wasm-util": {
- "version": "0.10.4",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
- "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==",
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
@@ -7048,7 +6790,6 @@
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7",
@@ -7062,7 +6803,6 @@
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/types": "^7.0.0"
}
@@ -7072,7 +6812,6 @@
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0"
@@ -7083,7 +6822,6 @@
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/types": "^7.28.2"
}
@@ -7288,11 +7026,12 @@
}
},
"node_modules/@types/node": {
- "version": "20.19.43",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
- "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
+ "version": "26.6.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz",
+ "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==",
+ "license": "MIT",
"dependencies": {
- "undici-types": "~6.21.0"
+ "undici-types": "~8.9.0"
}
},
"node_modules/@types/node-int64": {
@@ -7305,6 +7044,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/node/node_modules/undici-types": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
+ "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
+ "license": "MIT"
+ },
"node_modules/@types/oracledb": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.5.2.tgz",
@@ -7674,7 +7419,6 @@
"arm"
],
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"android"
@@ -7688,7 +7432,6 @@
"arm64"
],
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"android"
@@ -7702,7 +7445,6 @@
"arm64"
],
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -7716,7 +7458,6 @@
"x64"
],
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -7730,7 +7471,6 @@
"x64"
],
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"freebsd"
@@ -7744,7 +7484,6 @@
"arm"
],
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7758,7 +7497,6 @@
"arm"
],
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7772,10 +7510,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7789,10 +7523,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7806,10 +7536,6 @@
"loong64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7823,10 +7549,6 @@
"loong64"
],
"dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7840,10 +7562,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7857,10 +7575,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7874,10 +7588,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7891,10 +7601,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7908,10 +7614,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7925,10 +7627,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -7942,7 +7640,6 @@
"arm64"
],
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"openharmony"
@@ -7956,7 +7653,6 @@
"wasm32"
],
"dev": true,
- "license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.10.0",
@@ -7975,7 +7671,6 @@
"arm64"
],
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -7989,7 +7684,6 @@
"ia32"
],
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -8003,7 +7697,6 @@
"x64"
],
"dev": true,
- "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -8481,16 +8174,15 @@
}
},
"node_modules/babel-jest": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.2.tgz",
- "integrity": "sha512-ES8WeJ2fNWPEDunyAtUfJ12nHyaWUloCHdkfK2oW5uSoQSUmjNKdWiGaXJITJwvFEWoD/evHEg/QoCXAvLaQqQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz",
+ "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/transform": "30.5.2",
+ "@jest/transform": "30.4.1",
"@types/babel__core": "^7.20.5",
- "babel-plugin-istanbul": "^8.0.0",
- "babel-preset-jest": "30.5.0",
+ "babel-plugin-istanbul": "^7.0.1",
+ "babel-preset-jest": "30.4.0",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"slash": "^3.0.0"
@@ -8503,31 +8195,26 @@
}
},
"node_modules/babel-plugin-istanbul": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-8.0.0.tgz",
- "integrity": "sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz",
+ "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==",
"dev": true,
- "license": "BSD-3-Clause",
- "workspaces": [
- "test/babel-8"
- ],
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
"@istanbuljs/load-nyc-config": "^1.0.0",
"@istanbuljs/schema": "^0.1.3",
"istanbul-lib-instrument": "^6.0.2",
- "test-exclude": "^7.0.1"
+ "test-exclude": "^6.0.0"
},
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/babel-plugin-jest-hoist": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.5.0.tgz",
- "integrity": "sha512-gtGo1B+u14jrZQv6TdSWIWkTqclboo7Qn+dFAGUOIuXLFuJKAdg3U5MIWzZnW4vfUb4dX9skmAMiby86e/SF4A==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz",
+ "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/babel__core": "^7.20.5"
},
@@ -8562,20 +8249,19 @@
}
},
"node_modules/babel-preset-jest": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.5.0.tgz",
- "integrity": "sha512-ZGPn5ClP4lBDpuOK8W1yQIOy359HmbnZv3suucAlIe+SEE9yDsxV4S2PjSbH2Vc97U+WmzYD7vw3kr8NsQ/i6w==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz",
+ "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "babel-plugin-jest-hoist": "30.5.0",
+ "babel-plugin-jest-hoist": "30.4.0",
"babel-preset-current-node-syntax": "^1.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
},
"peerDependencies": {
- "@babel/core": "^7.11.0 || ^8.0.0-beta.1 || ^8.0.0"
+ "@babel/core": "^7.11.0 || ^8.0.0-beta.1"
}
},
"node_modules/balanced-match": {
@@ -8754,6 +8440,12 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/bowser": {
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
+ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
+ "license": "MIT"
+ },
"node_modules/brace-expansion": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
@@ -8987,7 +8679,6 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -9040,7 +8731,6 @@
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
"integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=10"
}
@@ -9210,7 +8900,6 @@
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
"integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
"dev": true,
- "license": "MIT",
"engines": {
"iojs": ">= 1.0.0",
"node": ">= 0.12.0"
@@ -9220,8 +8909,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz",
"integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/color": {
"version": "5.0.3",
@@ -9610,7 +9298,6 @@
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz",
"integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==",
"dev": true,
- "license": "MIT",
"peerDependencies": {
"babel-plugin-macros": "^3.1.0"
},
@@ -9703,8 +9390,8 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "devOptional": true,
"license": "Apache-2.0",
+ "optional": true,
"engines": {
"node": ">=8"
}
@@ -9714,7 +9401,6 @@
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -9844,7 +9530,6 @@
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
"integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=12"
},
@@ -10276,7 +9961,6 @@
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.0",
@@ -10299,32 +9983,29 @@
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
- "dev": true,
- "license": "ISC"
+ "dev": true
},
"node_modules/exit-x": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz",
"integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/expect": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.2.tgz",
- "integrity": "sha512-0LNuMvs8/5mRYhnCR4k+lGV7fqPMs6Bnksda4S3zsCUmdxBpAn4zU4NNzXdq1pMKe/H4W5t/zVIDSJ/H3e0vDA==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz",
+ "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/expect-utils": "30.5.2",
- "@jest/get-type": "30.5.0",
- "jest-matcher-utils": "30.5.2",
- "jest-message-util": "30.5.1",
- "jest-mock": "30.5.2",
- "jest-util": "30.5.1"
+ "@jest/expect-utils": "30.4.1",
+ "@jest/get-type": "30.1.0",
+ "jest-matcher-utils": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-mock": "30.4.1",
+ "jest-util": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -10436,6 +10117,23 @@
"node": ">=12.17.0"
}
},
+ "node_modules/fast-check/node_modules/pure-rand": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz",
+ "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -10521,24 +10219,6 @@
"bser": "2.1.1"
}
},
- "node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
@@ -11094,7 +10774,6 @@
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8.0.0"
}
@@ -11116,7 +10795,6 @@
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=10"
},
@@ -11395,8 +11073,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/http-errors": {
"version": "2.0.1",
@@ -11434,7 +11111,6 @@
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"dev": true,
- "license": "Apache-2.0",
"engines": {
"node": ">=10.17.0"
}
@@ -11533,7 +11209,6 @@
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
"integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"pkg-dir": "^4.2.0",
"resolve-cwd": "^3.0.0"
@@ -11634,33 +11309,6 @@
"url": "https://opencollective.com/ioredis"
}
},
- "node_modules/int53": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/int53/-/int53-0.2.4.tgz",
- "integrity": "sha512-a5jlKftS7HUOhkUyYD7j2sJ/ZnvWiNlZS1ldR+g1ifQ+/UuZXIE+YTc/lK1qGj/GwAU5F8Z0e1eVq2t1J5Ob2g==",
- "license": "BSD-3-Clause"
- },
- "node_modules/ioredis": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-6.0.0.tgz",
- "integrity": "sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==",
- "license": "MIT",
- "dependencies": {
- "@ioredis/commands": "2.0.0",
- "cluster-key-slot": "1.1.1",
- "debug": "4.4.3",
- "denque": "2.1.0",
- "redis-errors": "1.2.0",
- "standard-as-callback": "2.1.0"
- },
- "engines": {
- "node": ">=20.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/ioredis"
- }
- },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -11708,7 +11356,6 @@
"resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
"integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -11885,7 +11532,6 @@
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
"dev": true,
- "license": "BSD-3-Clause",
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
"make-dir": "^4.0.0",
@@ -11900,7 +11546,6 @@
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
"dev": true,
- "license": "BSD-3-Clause",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.23",
"debug": "^4.1.1",
@@ -11915,7 +11560,6 @@
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
"dev": true,
- "license": "BSD-3-Clause",
"dependencies": {
"html-escaper": "^2.0.0",
"istanbul-lib-report": "^3.0.0"
@@ -11947,16 +11591,15 @@
}
},
"node_modules/jest": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.2.tgz",
- "integrity": "sha512-3gnpdBIza8ijCl3iMV9ChE3hSt1+46865qqod863gQtJr1DixOkAU5DoGbi3u+XlRcw++BhZQMu/y4xBdHBvSQ==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz",
+ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/core": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/core": "30.4.2",
+ "@jest/types": "30.4.1",
"import-local": "^3.2.0",
- "jest-cli": "30.5.2"
+ "jest-cli": "30.4.2"
},
"bin": {
"jest": "bin/jest.js"
@@ -11974,14 +11617,13 @@
}
},
"node_modules/jest-changed-files": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.5.1.tgz",
- "integrity": "sha512-0+bvMM/ENhDI29Z8q1r4HxiDIi4G5tnBmSw4esfPQoj9q8Nik7KIyuxHkTVnnniJQf05SxpGaKDQ+4h28ynGPg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz",
+ "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"execa": "^5.1.1",
- "jest-util": "30.5.1",
+ "jest-util": "30.4.1",
"p-limit": "^3.1.0"
},
"engines": {
@@ -11989,29 +11631,28 @@
}
},
"node_modules/jest-circus": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.2.tgz",
- "integrity": "sha512-vffFgjs5JSJ9q4ds1vNW3klKYPJfYfIY61LT/zaZgvTeASJOWZUt8LBfHgIcV4rwxlPBLl/ePBGccHEclS4PlQ==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz",
+ "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/environment": "30.5.2",
- "@jest/expect": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/environment": "30.4.1",
+ "@jest/expect": "30.4.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"chalk": "^4.1.2",
"co": "^4.6.0",
"dedent": "^1.6.0",
"is-generator-fn": "^2.1.0",
- "jest-each": "30.5.2",
- "jest-matcher-utils": "30.5.2",
- "jest-message-util": "30.5.1",
- "jest-runtime": "30.5.2",
- "jest-snapshot": "30.5.2",
- "jest-util": "30.5.1",
+ "jest-each": "30.4.1",
+ "jest-matcher-utils": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-runtime": "30.4.2",
+ "jest-snapshot": "30.4.1",
+ "jest-util": "30.4.1",
"p-limit": "^3.1.0",
- "pretty-format": "30.5.1",
+ "pretty-format": "30.4.1",
"pure-rand": "^7.0.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
@@ -12020,39 +11661,21 @@
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
- "node_modules/jest-circus/node_modules/pure-rand": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
- "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==",
- "dev": true,
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/dubzzz"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fast-check"
- }
- ],
- "license": "MIT"
- },
"node_modules/jest-cli": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.2.tgz",
- "integrity": "sha512-YYYxUUVjFgPvgEleKNKMaYGIFgR3l3832Cl0kRL/oCiDZ03v7wImUtquHl2OOBCCNgprjZgBN5bczS/JklPBDg==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz",
+ "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/core": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/core": "30.4.2",
+ "@jest/test-result": "30.4.1",
+ "@jest/types": "30.4.1",
"chalk": "^4.1.2",
"exit-x": "^0.2.2",
"import-local": "^3.2.0",
- "jest-config": "30.5.2",
- "jest-util": "30.5.1",
- "jest-validate": "30.5.1",
+ "jest-config": "30.4.2",
+ "jest-util": "30.4.1",
+ "jest-validate": "30.4.1",
"yargs": "^17.7.2"
},
"bin": {
@@ -12071,33 +11694,32 @@
}
},
"node_modules/jest-config": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.2.tgz",
- "integrity": "sha512-U6221OZekabePvGcGPf4raIBKMvYQWFHvhobDJRojOYaFMj2HaGytAdknjDhYc6JTz2fdHW9+6k0wWUDaNeOFQ==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz",
+ "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
- "@jest/get-type": "30.5.0",
- "@jest/pattern": "30.5.0",
- "@jest/test-sequencer": "30.5.2",
- "@jest/types": "30.5.1",
- "babel-jest": "30.5.2",
+ "@jest/get-type": "30.1.0",
+ "@jest/pattern": "30.4.0",
+ "@jest/test-sequencer": "30.4.1",
+ "@jest/types": "30.4.1",
+ "babel-jest": "30.4.1",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"deepmerge": "^4.3.1",
- "glob": "^13.0.6",
+ "glob": "^10.5.0",
"graceful-fs": "^4.2.11",
- "jest-circus": "30.5.2",
- "jest-docblock": "30.5.0",
- "jest-environment-node": "30.5.2",
- "jest-regex-util": "30.5.0",
- "jest-resolve": "30.5.1",
- "jest-runner": "30.5.2",
- "jest-util": "30.5.1",
- "jest-validate": "30.5.1",
+ "jest-circus": "30.4.2",
+ "jest-docblock": "30.4.0",
+ "jest-environment-node": "30.4.1",
+ "jest-regex-util": "30.4.0",
+ "jest-resolve": "30.4.1",
+ "jest-runner": "30.4.2",
+ "jest-util": "30.4.1",
+ "jest-validate": "30.4.1",
"parse-json": "^5.2.0",
- "pretty-format": "30.5.1",
+ "pretty-format": "30.4.1",
"slash": "^3.0.0",
"strip-json-comments": "^3.1.1"
},
@@ -12121,28 +11743,69 @@
}
}
},
+ "node_modules/jest-config/node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "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",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jest-config/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
+ "node_modules/jest-config/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/jest-diff": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.2.tgz",
- "integrity": "sha512-rBtHnI6BWTiWj3lM7fOLVfChx7R9B0u9VV3PRHawKs79x6ZjW1QoSrKenaigNGWlvhtoi2BqvDbABDie+Os3xQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz",
+ "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/diff-sequences": "30.5.0",
- "@jest/get-type": "30.5.0",
+ "@jest/diff-sequences": "30.4.0",
+ "@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
- "pretty-format": "30.5.1"
+ "pretty-format": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-docblock": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.5.0.tgz",
- "integrity": "sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz",
+ "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"detect-newline": "^3.1.0"
},
@@ -12151,110 +11814,105 @@
}
},
"node_modules/jest-each": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.2.tgz",
- "integrity": "sha512-GRrW+7fmmgmkvyfEBqc1QFdWnF1Ex5yG6Y6AWL4TTxnkWlKIT98dsKh3P3UuWm6v4XsQwVE6DEzXIPMzWJ3ZbA==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz",
+ "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/get-type": "30.5.0",
- "@jest/types": "30.5.1",
+ "@jest/get-type": "30.1.0",
+ "@jest/types": "30.4.1",
"chalk": "^4.1.2",
- "jest-regex-util": "30.5.0",
- "jest-util": "30.5.1",
- "pretty-format": "30.5.1"
+ "jest-util": "30.4.1",
+ "pretty-format": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-environment-node": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.2.tgz",
- "integrity": "sha512-Ciz4KgTGIbojxSKpuFImgWJj5EyqfE8xMq9NxWmOXnQywHHS03H9Mv+vaCB29J5pr3JBrY27WbRV18rBdjh8Dg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz",
+ "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/environment": "30.5.2",
- "@jest/fake-timers": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/environment": "30.4.1",
+ "@jest/fake-timers": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
- "jest-mock": "30.5.2",
- "jest-util": "30.5.1",
- "jest-validate": "30.5.1"
+ "jest-mock": "30.4.1",
+ "jest-util": "30.4.1",
+ "jest-validate": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-haste-map": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.5.1.tgz",
- "integrity": "sha512-VIFgt67jW480YDxKfEv9IYQKrFpYt7bOCMn3VsnjK7AK9qo7p6acpnA1DHwsVOULE3dTaYew/6JaTD/d0VDHoQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz",
+ "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/types": "30.5.1",
- "@parcel/watcher": "^2.6.0",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
- "fdir": "^6.5.0",
"graceful-fs": "^4.2.11",
- "jest-regex-util": "30.5.0",
- "jest-util": "30.5.1",
- "jest-worker": "30.5.1",
- "picomatch": "^4.0.3"
+ "jest-regex-util": "30.4.0",
+ "jest-util": "30.4.1",
+ "jest-worker": "30.4.1",
+ "picomatch": "^4.0.3",
+ "walker": "^1.0.8"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.3"
}
},
"node_modules/jest-leak-detector": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.5.1.tgz",
- "integrity": "sha512-gt4GT2aWgEoCNTcBe4rqS74xTIUJxi+UD9SNSu9aOk5LmTEd6fS5KSTmAKAfitCCktQHNN+upUuL6EkBfFbMDQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz",
+ "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/get-type": "30.5.0",
- "pretty-format": "30.5.1"
+ "@jest/get-type": "30.1.0",
+ "pretty-format": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-matcher-utils": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.2.tgz",
- "integrity": "sha512-lFkB365PTIHOhPzwrb9T6gvlDnPpOZ7/c3ewOKKB7bzztqjVrtlyRL2MkyfJXpyY2u2SKw001JKrTx14fBUFcQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz",
+ "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/get-type": "30.5.0",
+ "@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
- "jest-diff": "30.5.2",
- "pretty-format": "30.5.1"
+ "jest-diff": "30.4.1",
+ "pretty-format": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-message-util": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.1.tgz",
- "integrity": "sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz",
+ "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
- "jest-util": "30.5.1",
+ "jest-util": "30.4.1",
"picomatch": "^4.0.3",
- "pretty-format": "30.5.1",
+ "pretty-format": "30.4.1",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
@@ -12263,126 +11921,136 @@
}
},
"node_modules/jest-mock": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.2.tgz",
- "integrity": "sha512-KTHb37Fhj8xY8qPFvTFaFJHcdsumQp+ExsUlgoX232Agmc706gagBs53+CYvulV7MVQo9AgXOUxsqzzVBoJlrA==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz",
+ "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/expect-utils": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
- "jest-util": "30.5.1"
+ "jest-util": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
+ "node_modules/jest-pnp-resolver": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+ "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "jest-resolve": "*"
+ },
+ "peerDependenciesMeta": {
+ "jest-resolve": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jest-regex-util": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.5.0.tgz",
- "integrity": "sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz",
+ "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-resolve": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.5.1.tgz",
- "integrity": "sha512-wprhLejRtwN6h8ZgaqC0eYjGJ1uMdGPT/+b3eFncbG4NWuuP9QL+vWwRinu9waw7hkOLcpMJGVJAMgBwsPssMQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz",
+ "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.5.1",
- "jest-util": "30.5.1",
- "jest-validate": "30.5.1",
+ "jest-haste-map": "30.4.1",
+ "jest-pnp-resolver": "^1.2.3",
+ "jest-util": "30.4.1",
+ "jest-validate": "30.4.1",
"slash": "^3.0.0",
- "unrs-resolver": "^1.12.1"
+ "unrs-resolver": "^1.7.11"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-resolve-dependencies": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.2.tgz",
- "integrity": "sha512-GMI0DP5enX9Z2qW2zFy1bOkrCfWAOPRJQ7qHt0pdfrxBPsgWxglMPrRSK2TX/wRWYxcPRSLFg3Z88qPOzHx7FQ==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz",
+ "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "jest-regex-util": "30.5.0",
- "jest-snapshot": "30.5.2"
+ "jest-regex-util": "30.4.0",
+ "jest-snapshot": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-runner": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.2.tgz",
- "integrity": "sha512-/B6px7hiiNhVSRwuv9AGn0nawYvwsHeQZ3ItTd5Gp2diFd0EOKaWVeWqcbw3L88vx1GfpCKjQkxWItZCTl11Rw==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz",
+ "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/console": "30.5.2",
- "@jest/environment": "30.5.2",
- "@jest/source-map": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/transform": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/console": "30.4.1",
+ "@jest/environment": "30.4.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/transform": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"chalk": "^4.1.2",
"emittery": "^0.13.1",
"exit-x": "^0.2.2",
"graceful-fs": "^4.2.11",
- "jest-docblock": "30.5.0",
- "jest-environment-node": "30.5.2",
- "jest-haste-map": "30.5.1",
- "jest-leak-detector": "30.5.1",
- "jest-message-util": "30.5.1",
- "jest-resolve": "30.5.1",
- "jest-runtime": "30.5.2",
- "jest-util": "30.5.1",
- "jest-watcher": "30.5.2",
- "jest-worker": "30.5.1",
- "p-limit": "^3.1.0"
+ "jest-docblock": "30.4.0",
+ "jest-environment-node": "30.4.1",
+ "jest-haste-map": "30.4.1",
+ "jest-leak-detector": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-resolve": "30.4.1",
+ "jest-runtime": "30.4.2",
+ "jest-util": "30.4.1",
+ "jest-watcher": "30.4.1",
+ "jest-worker": "30.4.1",
+ "p-limit": "^3.1.0",
+ "source-map-support": "0.5.13"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-runtime": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.2.tgz",
- "integrity": "sha512-0paUAHBlheai10A3XSy9Xh+YBG0IROy+/qzcDPzQ/nbbXrznQzeWy5qniICWK6d3W3scaRQCVAbXdcK0ibBq4w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/environment": "30.5.2",
- "@jest/fake-timers": "30.5.2",
- "@jest/globals": "30.5.2",
- "@jest/source-map": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/transform": "30.5.2",
- "@jest/types": "30.5.1",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz",
+ "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "30.4.1",
+ "@jest/fake-timers": "30.4.1",
+ "@jest/globals": "30.4.1",
+ "@jest/source-map": "30.0.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/transform": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"chalk": "^4.1.2",
- "cjs-module-lexer": "^2.2.0",
+ "cjs-module-lexer": "^2.1.0",
"collect-v8-coverage": "^1.0.2",
- "es-module-lexer": "^2.1.0",
- "glob": "^13.0.6",
+ "glob": "^10.5.0",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.5.1",
- "jest-message-util": "30.5.1",
- "jest-mock": "30.5.2",
- "jest-regex-util": "30.5.0",
- "jest-resolve": "30.5.1",
- "jest-snapshot": "30.5.2",
- "jest-util": "30.5.1",
+ "jest-haste-map": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-mock": "30.4.1",
+ "jest-regex-util": "30.4.0",
+ "jest-resolve": "30.4.1",
+ "jest-snapshot": "30.4.1",
+ "jest-util": "30.4.1",
"slash": "^3.0.0",
"strip-bom": "^4.0.0"
},
@@ -12390,32 +12058,74 @@
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
+ "node_modules/jest-runtime/node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "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",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
+ "node_modules/jest-runtime/node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/jest-snapshot": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.2.tgz",
- "integrity": "sha512-jKIdes25AnAH73dOCi2qE6J6vHJ6L2vyBS11M89kT50DVJcLTf2AOjvcc+2vVaN8lUbY5v2R83aqXo3Ro9fqYA==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz",
+ "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@babel/generator": "^7.27.5",
"@babel/plugin-syntax-jsx": "^7.27.1",
"@babel/plugin-syntax-typescript": "^7.27.1",
"@babel/types": "^7.27.3",
- "@jest/expect-utils": "30.5.2",
- "@jest/get-type": "30.5.0",
- "@jest/snapshot-utils": "30.5.1",
- "@jest/transform": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/expect-utils": "30.4.1",
+ "@jest/get-type": "30.1.0",
+ "@jest/snapshot-utils": "30.4.1",
+ "@jest/transform": "30.4.1",
+ "@jest/types": "30.4.1",
"babel-preset-current-node-syntax": "^1.2.0",
"chalk": "^4.1.2",
- "expect": "30.5.2",
+ "expect": "30.4.1",
"graceful-fs": "^4.2.11",
- "jest-diff": "30.5.2",
- "jest-matcher-utils": "30.5.2",
- "jest-message-util": "30.5.1",
- "jest-util": "30.5.1",
- "pretty-format": "30.5.1",
+ "jest-diff": "30.4.1",
+ "jest-matcher-utils": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-util": "30.4.1",
+ "pretty-format": "30.4.1",
"semver": "^7.7.2",
"synckit": "^0.11.8"
},
@@ -12424,13 +12134,12 @@
}
},
"node_modules/jest-util": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.1.tgz",
- "integrity": "sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz",
+ "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
@@ -12442,18 +12151,17 @@
}
},
"node_modules/jest-validate": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.5.1.tgz",
- "integrity": "sha512-i/buJ56wTpxihE93hQYNMfdOThS87+HGvLZzZJmU4xggPcdTYQq051iwALLCHp3q+SkCGH+EFejQZz5EbBSkkg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz",
+ "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/get-type": "30.5.0",
- "@jest/types": "30.5.1",
+ "@jest/get-type": "30.1.0",
+ "@jest/types": "30.4.1",
"camelcase": "^6.3.0",
"chalk": "^4.1.2",
"leven": "^3.1.0",
- "pretty-format": "30.5.1"
+ "pretty-format": "30.4.1"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -12464,7 +12172,6 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=10"
},
@@ -12473,19 +12180,18 @@
}
},
"node_modules/jest-watcher": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.2.tgz",
- "integrity": "sha512-rqRgg4R11GA9m1UsEZwQeixHsgZReCn6TOHAHtIH33yXrIvoX0OGPA20Yzzjkz7X40fZvcRSxEpQppYPcWzmcQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz",
+ "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/test-result": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"ansi-escapes": "^4.3.2",
"chalk": "^4.1.2",
"emittery": "^0.13.1",
- "jest-util": "30.5.1",
+ "jest-util": "30.4.1",
"string-length": "^4.0.2"
},
"engines": {
@@ -12493,15 +12199,14 @@
}
},
"node_modules/jest-worker": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.5.1.tgz",
- "integrity": "sha512-Cbxh5v7AoLuFRmFJSM4/aHdQ68rjXvUWr716EE0Dh3I7T+T/3FgFKhOERGXHcU2Meftq9+9zxPM3TSNyI9D+HA==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz",
+ "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/node": "*",
"@ungap/structured-clone": "^1.3.0",
- "jest-util": "30.5.1",
+ "jest-util": "30.4.1",
"merge-stream": "^2.0.0",
"supports-color": "^8.1.1"
},
@@ -12514,7 +12219,6 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -12717,7 +12421,6 @@
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -12960,7 +12663,6 @@
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
@@ -12977,6 +12679,15 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
+ "node_modules/makeerror": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
+ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
+ "dev": true,
+ "dependencies": {
+ "tmpl": "1.0.5"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -13135,19 +12846,6 @@
"node": ">=6"
}
},
- "node_modules/mimic-function": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
- "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -13323,7 +13021,6 @@
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
"integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
"dev": true,
- "license": "MIT",
"bin": {
"napi-postinstall": "lib/cli.js"
},
@@ -13359,13 +13056,6 @@
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="
},
- "node_modules/node-addon-api": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
- "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -13456,7 +13146,6 @@
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"path-key": "^3.0.0"
},
@@ -13721,7 +13410,6 @@
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -13901,11 +13589,15 @@
},
"node_modules/pg-cloudflare": {
"version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
+ "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
+ "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
"license": "MIT"
},
"node_modules/pg-int8": {
@@ -13989,7 +13681,6 @@
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"find-up": "^4.0.0"
},
@@ -14002,7 +13693,6 @@
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
@@ -14016,7 +13706,6 @@
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
@@ -14029,7 +13718,6 @@
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
- "license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
@@ -14045,7 +13733,6 @@
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
@@ -14119,16 +13806,15 @@
}
},
"node_modules/pretty-format": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz",
- "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz",
+ "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@jest/react-is-18": "npm:react-is@^18.3.1",
- "@jest/react-is-19": "npm:react-is@^19.2.5",
- "@jest/schemas": "30.5.0",
- "ansi-styles": "^5.2.0"
+ "@jest/schemas": "30.4.1",
+ "ansi-styles": "^5.2.0",
+ "react-is-18": "npm:react-is@^18.3.1",
+ "react-is-19": "npm:react-is@^19.2.5"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -14258,9 +13944,9 @@
}
},
"node_modules/pure-rand": {
- "version": "8.4.2",
- "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz",
- "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
+ "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==",
"dev": true,
"funding": [
{
@@ -14271,8 +13957,7 @@
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
- ],
- "license": "MIT"
+ ]
},
"node_modules/q": {
"version": "1.5.1",
@@ -14354,6 +14039,20 @@
"node": ">= 0.10"
}
},
+ "node_modules/react-is-18": {
+ "name": "react-is",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true
+ },
+ "node_modules/react-is-19": {
+ "name": "react-is",
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz",
+ "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==",
+ "dev": true
+ },
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -14450,7 +14149,6 @@
"resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
"integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"resolve-from": "^5.0.0"
},
@@ -14463,7 +14161,6 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -14923,6 +14620,25 @@
"node": ">= 8"
}
},
+ "node_modules/source-map-support": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
+ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/source-map-support/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -14936,8 +14652,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
- "dev": true,
- "license": "BSD-3-Clause"
+ "dev": true
},
"node_modules/stack-trace": {
"version": "0.0.10",
@@ -14982,19 +14697,6 @@
"node": ">= 0.8"
}
},
- "node_modules/stdin-discarder": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz",
- "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -15016,7 +14718,6 @@
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
"integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"char-regex": "^1.0.2",
"strip-ansi": "^6.0.0"
@@ -15080,7 +14781,6 @@
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
"integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -15090,7 +14790,6 @@
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -15190,7 +14889,6 @@
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz",
"integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@pkgr/core": "^0.3.6"
},
@@ -15418,119 +15116,60 @@
}
},
"node_modules/test-exclude": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
- "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
"dev": true,
- "license": "ISC",
"dependencies": {
"@istanbuljs/schema": "^0.1.2",
- "glob": "^10.4.1",
- "minimatch": "^10.2.2"
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
},
"engines": {
- "node": ">=18"
+ "node": ">=8"
}
},
- "node_modules/test-exclude/node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "node_modules/test-exclude/node_modules/brace-expansion": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
- "license": "MIT",
- "engines": {
- "node": "18 || 20 || >=22"
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
}
},
"node_modules/test-exclude/node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"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",
"dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
- "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
- "dev": true,
- "license": "ISC",
"dependencies": {
- "brace-expansion": "^2.0.2"
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/test-exclude/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/test-exclude/node_modules/minimatch": {
- "version": "10.2.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
- "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "brace-expansion": "^5.0.8"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/test-exclude/node_modules/minimatch/node_modules/brace-expansion": {
- "version": "5.0.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
- "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/test-exclude/node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
- "license": "BlueOak-1.0.0",
"dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ "brace-expansion": "^1.1.7"
},
"engines": {
- "node": ">=16 || 14 >=14.18"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "node": "*"
}
},
"node_modules/text-extensions": {
@@ -15601,6 +15240,12 @@
"node": ">=0.6.0"
}
},
+ "node_modules/tmpl": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
+ "dev": true
+ },
"node_modules/to-buffer": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
@@ -15843,7 +15488,6 @@
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=4"
}
@@ -16001,7 +15645,8 @@
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true
},
"node_modules/unicorn-magic": {
"version": "0.3.0",
@@ -16039,7 +15684,6 @@
"integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==",
"dev": true,
"hasInstallScript": true,
- "license": "MIT",
"dependencies": {
"napi-postinstall": "^0.3.4"
},
@@ -16145,7 +15789,6 @@
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
"integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
"dev": true,
- "license": "ISC",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.12",
"@types/istanbul-lib-coverage": "^2.0.1",
@@ -16242,6 +15885,15 @@
}
}
},
+ "node_modules/walker": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
+ "dev": true,
+ "dependencies": {
+ "makeerror": "1.0.12"
+ }
+ },
"node_modules/watchpack": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz",
@@ -16797,52 +16449,288 @@
"resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.18.1.tgz",
"integrity": "sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ==",
"requires": {
- "@types/estree": "^1.0.8",
- "astring": "^1.9.0",
- "esquery": "^1.7.0",
- "meriyah": "^6.1.4",
- "semifies": "^1.0.0",
- "source-map": "^0.6.0"
- },
- "dependencies": {
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
- }
+ "@types/estree": "^1.0.8",
+ "astring": "^1.9.0",
+ "esquery": "^1.7.0",
+ "meriyah": "^6.1.4",
+ "semifies": "^1.0.0",
+ "source-map": "^0.6.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "@apm-js-collab/code-transformer-bundler-plugins": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer-bundler-plugins/-/code-transformer-bundler-plugins-0.7.3.tgz",
+ "integrity": "sha512-qNbPwuMZ8f5ZuGj/ttPeB7a6C/S1bB6tNYaEL5vNiRKydSAxa4AU0gxCWgaP4fVju+AuwhcumSFjrEcGF9Dv7Q==",
+ "requires": {
+ "@apm-js-collab/code-transformer": "^0.18.0",
+ "es-module-lexer": "^2.1.0",
+ "magic-string": "^0.30.21",
+ "module-details-from-path": "^1.0.4"
+ },
+ "dependencies": {
+ "magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "requires": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ }
+ }
+ },
+ "@apm-js-collab/tracing-hooks": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.13.0.tgz",
+ "integrity": "sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==",
+ "requires": {
+ "@apm-js-collab/code-transformer": "^0.18.0",
+ "debug": "^4.4.1",
+ "module-details-from-path": "^1.0.4"
+ }
+ },
+ "@aws-sdk/checksums": {
+ "version": "3.1001.1",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.1.tgz",
+ "integrity": "sha512-x12Q17KYlJAd3nKf8LV5LV0vt8sh8/6YfQLGPtrGnQf/tW4jqxPGq5GPpuVitpQYM3eUR4XB7CbxZf751NMbLw==",
+ "requires": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/client-s3": {
+ "version": "3.1145.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1145.0.tgz",
+ "integrity": "sha512-MPKAQdx8qCZW1/ChDPILNehtAn/BMP4GNTnaH/xhGCWg3v50ADVq1K2WzxIrFfeANdWUyjwiY0CXDBhclsNWgQ==",
+ "requires": {
+ "@aws-sdk/checksums": "^3.1001.1",
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/credential-provider-node": "^3.972.84",
+ "@aws-sdk/middleware-sdk-s3": "^3.972.77",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.47",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/fetch-http-handler": "^5.8.0",
+ "@smithy/node-http-handler": "^4.12.1",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/core": {
+ "version": "3.978.1",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz",
+ "integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==",
+ "requires": {
+ "@aws-sdk/types": "^3.974.6",
+ "@aws-sdk/xml-builder": "^3.972.41",
+ "@aws/lambda-invoke-store": "^0.3.0",
+ "@smithy/core": "^3.35.0",
+ "@smithy/signature-v4": "^5.7.3",
+ "@smithy/types": "^4.19.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/credential-provider-env": {
+ "version": "3.972.72",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz",
+ "integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==",
+ "requires": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/credential-provider-http": {
+ "version": "3.972.74",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz",
+ "integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==",
+ "requires": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/fetch-http-handler": "^5.8.0",
+ "@smithy/node-http-handler": "^4.12.1",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/credential-provider-ini": {
+ "version": "3.973.17",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz",
+ "integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==",
+ "requires": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/credential-provider-env": "^3.972.72",
+ "@aws-sdk/credential-provider-http": "^3.972.74",
+ "@aws-sdk/credential-provider-login": "^3.972.79",
+ "@aws-sdk/credential-provider-process": "^3.972.72",
+ "@aws-sdk/credential-provider-sso": "^3.973.16",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.78",
+ "@aws-sdk/nested-clients": "^3.997.46",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/credential-provider-imds": "^4.5.2",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/credential-provider-login": {
+ "version": "3.972.79",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz",
+ "integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==",
+ "requires": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/nested-clients": "^3.997.46",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/credential-provider-node": {
+ "version": "3.972.84",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz",
+ "integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==",
+ "requires": {
+ "@aws-sdk/credential-provider-env": "^3.972.72",
+ "@aws-sdk/credential-provider-http": "^3.972.74",
+ "@aws-sdk/credential-provider-ini": "^3.973.17",
+ "@aws-sdk/credential-provider-process": "^3.972.72",
+ "@aws-sdk/credential-provider-sso": "^3.973.16",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.78",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/credential-provider-imds": "^4.5.2",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/credential-provider-process": {
+ "version": "3.972.72",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz",
+ "integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==",
+ "requires": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/credential-provider-sso": {
+ "version": "3.973.16",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz",
+ "integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==",
+ "requires": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/nested-clients": "^3.997.46",
+ "@aws-sdk/token-providers": "3.1138.0",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/credential-provider-web-identity": {
+ "version": "3.972.78",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz",
+ "integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==",
+ "requires": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/nested-clients": "^3.997.46",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/middleware-sdk-s3": {
+ "version": "3.972.77",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.77.tgz",
+ "integrity": "sha512-E7W2UOeUoc+lg3uIfR/dM7ZwusHwhBQrKMnlkRv4EXRR+C0YtV1pg25xC7GdZIhXH+NAMgZPCbE7o5to2cjFiw==",
+ "requires": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.47",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/nested-clients": {
+ "version": "3.997.46",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz",
+ "integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==",
+ "requires": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.47",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/fetch-http-handler": "^5.8.0",
+ "@smithy/node-http-handler": "^4.12.1",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/signature-v4-multi-region": {
+ "version": "3.996.47",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz",
+ "integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==",
+ "requires": {
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/signature-v4": "^5.7.3",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@aws-sdk/token-providers": {
+ "version": "3.1138.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz",
+ "integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==",
+ "requires": {
+ "@aws-sdk/core": "^3.978.1",
+ "@aws-sdk/nested-clients": "^3.997.46",
+ "@aws-sdk/types": "^3.974.6",
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
}
},
- "@apm-js-collab/code-transformer-bundler-plugins": {
- "version": "0.7.3",
- "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer-bundler-plugins/-/code-transformer-bundler-plugins-0.7.3.tgz",
- "integrity": "sha512-qNbPwuMZ8f5ZuGj/ttPeB7a6C/S1bB6tNYaEL5vNiRKydSAxa4AU0gxCWgaP4fVju+AuwhcumSFjrEcGF9Dv7Q==",
+ "@aws-sdk/types": {
+ "version": "3.974.6",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz",
+ "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==",
"requires": {
- "@apm-js-collab/code-transformer": "^0.18.0",
- "es-module-lexer": "^2.1.0",
- "magic-string": "^0.30.21",
- "module-details-from-path": "^1.0.4"
- },
- "dependencies": {
- "magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "requires": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- }
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
}
},
- "@apm-js-collab/tracing-hooks": {
- "version": "0.13.0",
- "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.13.0.tgz",
- "integrity": "sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==",
+ "@aws-sdk/xml-builder": {
+ "version": "3.972.41",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz",
+ "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==",
"requires": {
- "@apm-js-collab/code-transformer": "^0.18.0",
- "debug": "^4.4.1",
- "module-details-from-path": "^1.0.4"
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
}
},
+ "@aws/lambda-invoke-store": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
+ "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="
+ },
"@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -18364,9 +18252,9 @@
}
},
"js-yaml": {
- "version": "3.15.2",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz",
- "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==",
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
+ "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
@@ -18415,31 +18303,31 @@
"dev": true
},
"@jest/console": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.2.tgz",
- "integrity": "sha512-e8sQC4pCMHPBrPX7Hk8TJr+DM5dkUnImSTNag8SEWFAFAF6xIz4AJvCKapnrdHnZxep4bxw4PhG+ZbimYBMyKw==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz",
+ "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==",
"dev": true,
"requires": {
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"chalk": "^4.1.2",
- "jest-message-util": "30.5.1",
- "jest-util": "30.5.1",
+ "jest-message-util": "30.4.1",
+ "jest-util": "30.4.1",
"slash": "^3.0.0"
}
},
"@jest/core": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.2.tgz",
- "integrity": "sha512-/jqvFWoJF7LV1a6AUpYMbMm+2yIYsHJfR474H0SCzr9k8tnO/jl9wRg6zSG6lxgf6EozNlCG4amFzWsbZSvvZA==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz",
+ "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==",
"dev": true,
"requires": {
- "@jest/console": "30.5.2",
- "@jest/pattern": "30.5.0",
- "@jest/reporters": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/transform": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/console": "30.4.1",
+ "@jest/pattern": "30.4.0",
+ "@jest/reporters": "30.4.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/transform": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"ansi-escapes": "^4.3.2",
"chalk": "^4.1.2",
@@ -18447,232 +18335,251 @@
"exit-x": "^0.2.2",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
- "jest-changed-files": "30.5.1",
- "jest-config": "30.5.2",
- "jest-haste-map": "30.5.1",
- "jest-message-util": "30.5.1",
- "jest-regex-util": "30.5.0",
- "jest-resolve": "30.5.1",
- "jest-resolve-dependencies": "30.5.2",
- "jest-runner": "30.5.2",
- "jest-runtime": "30.5.2",
- "jest-snapshot": "30.5.2",
- "jest-util": "30.5.1",
- "jest-validate": "30.5.1",
- "jest-watcher": "30.5.2",
- "pretty-format": "30.5.1",
+ "jest-changed-files": "30.4.1",
+ "jest-config": "30.4.2",
+ "jest-haste-map": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-regex-util": "30.4.0",
+ "jest-resolve": "30.4.1",
+ "jest-resolve-dependencies": "30.4.2",
+ "jest-runner": "30.4.2",
+ "jest-runtime": "30.4.2",
+ "jest-snapshot": "30.4.1",
+ "jest-util": "30.4.1",
+ "jest-validate": "30.4.1",
+ "jest-watcher": "30.4.1",
+ "pretty-format": "30.4.1",
"slash": "^3.0.0"
}
},
"@jest/diff-sequences": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz",
- "integrity": "sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz",
+ "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==",
"dev": true
},
"@jest/environment": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.2.tgz",
- "integrity": "sha512-nwFPOUMbmsjNNRCpnJpB9HbSZu4C07wDDds++5j+j1MezzPLxMPDdbdUVjr2o4RULDOValnDd70taPFI1EtxAg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz",
+ "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==",
"dev": true,
"requires": {
- "@jest/fake-timers": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/fake-timers": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
- "jest-mock": "30.5.2"
+ "jest-mock": "30.4.1"
}
},
"@jest/expect": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.5.2.tgz",
- "integrity": "sha512-2E+1Pg6jJxbRjLNLpciC71iri/3qsKJL222/aXJEFwC0VdUSvfS+JZQQLEVjUYYc8t8pQu2MGE3cF2+bNTYRsg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz",
+ "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==",
"dev": true,
"requires": {
- "expect": "30.5.2",
- "jest-snapshot": "30.5.2"
+ "expect": "30.4.1",
+ "jest-snapshot": "30.4.1"
}
},
"@jest/expect-utils": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.2.tgz",
- "integrity": "sha512-d/HOLSrJUlBIl6n3LzOa7cjW3xdePm5H5gQw2lixZ/0h157l9sw3mLblzFqnPcTbhDzhRQdBOX3A3KcG73nm5Q==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz",
+ "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==",
"dev": true,
"requires": {
- "@jest/get-type": "30.5.0"
+ "@jest/get-type": "30.1.0"
}
},
"@jest/fake-timers": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.2.tgz",
- "integrity": "sha512-GKk0h0tKT5SpDPxDhPg3r9mm5s8/YCBzVNMOGdRiQXwUiMsjU10mVPlMaQstH6/cu50vOb8N9oQUMETHuwwxKw==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz",
+ "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==",
"dev": true,
"requires": {
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"@sinonjs/fake-timers": "^15.4.0",
"@types/node": "*",
- "jest-message-util": "30.5.1",
- "jest-mock": "30.5.2",
- "jest-util": "30.5.1"
+ "jest-message-util": "30.4.1",
+ "jest-mock": "30.4.1",
+ "jest-util": "30.4.1"
}
},
"@jest/get-type": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.5.0.tgz",
- "integrity": "sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==",
+ "version": "30.1.0",
+ "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz",
+ "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==",
"dev": true
},
"@jest/globals": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.5.2.tgz",
- "integrity": "sha512-62zfS+dL+M5aTNHXCNLyqD+j4oqxTIzDYrZbpqMP8Yn+gvlAGyzC6BSPNb1ZmFOakV1RtFYX/O3FCQXSWmQdgg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz",
+ "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==",
"dev": true,
"requires": {
- "@jest/environment": "30.5.2",
- "@jest/expect": "30.5.2",
- "@jest/types": "30.5.1",
- "jest-mock": "30.5.2"
+ "@jest/environment": "30.4.1",
+ "@jest/expect": "30.4.1",
+ "@jest/types": "30.4.1",
+ "jest-mock": "30.4.1"
}
},
"@jest/pattern": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.5.0.tgz",
- "integrity": "sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz",
+ "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==",
"dev": true,
"requires": {
"@types/node": "*",
- "jest-regex-util": "30.5.0"
+ "jest-regex-util": "30.4.0"
}
},
- "@jest/react-is-18": {
- "version": "npm:react-is@18.3.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
- "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
- "dev": true
- },
- "@jest/react-is-19": {
- "version": "npm:react-is@19.3.0",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.0.tgz",
- "integrity": "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==",
- "dev": true
- },
"@jest/reporters": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.5.2.tgz",
- "integrity": "sha512-ev6E9iG73twQcfAfoTnviuYkr3yOrYbzAtMFCimv2fxHXPIp9PBt2u1wNMt0aYWgl8FPwIcS8oqRJg6alnht4Q==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz",
+ "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==",
"dev": true,
"requires": {
"@bcoe/v8-coverage": "^0.2.3",
- "@jest/console": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/transform": "30.5.2",
- "@jest/types": "30.5.1",
- "@jridgewell/trace-mapping": "^0.3.31",
+ "@jest/console": "30.4.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/transform": "30.4.1",
+ "@jest/types": "30.4.1",
+ "@jridgewell/trace-mapping": "^0.3.25",
"@types/node": "*",
"chalk": "^4.1.2",
"collect-v8-coverage": "^1.0.2",
"exit-x": "^0.2.2",
- "glob": "^13.0.6",
+ "glob": "^10.5.0",
"graceful-fs": "^4.2.11",
"istanbul-lib-coverage": "^3.0.0",
"istanbul-lib-instrument": "^6.0.0",
"istanbul-lib-report": "^3.0.0",
"istanbul-lib-source-maps": "^5.0.0",
"istanbul-reports": "^3.1.3",
- "jest-message-util": "30.5.1",
- "jest-util": "30.5.1",
- "jest-worker": "30.5.1",
+ "jest-message-util": "30.4.1",
+ "jest-util": "30.4.1",
+ "jest-worker": "30.4.1",
"slash": "^3.0.0",
"string-length": "^4.0.2",
"v8-to-istanbul": "^9.0.1"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "dev": true,
+ "requires": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ }
+ },
+ "lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
+ "path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ }
+ }
}
},
"@jest/schemas": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz",
- "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz",
+ "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==",
"dev": true,
"requires": {
"@sinclair/typebox": "^0.34.0"
}
},
"@jest/snapshot-utils": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.5.1.tgz",
- "integrity": "sha512-V3wnxNtiVmw5PPVg433Cn3VdXnsOeu/ofLw3KC04Bn2y1wIlU5kozXQn48rhrgj/02LrCVtntTEF1yBha0XSUw==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz",
+ "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==",
"dev": true,
"requires": {
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"natural-compare": "^1.4.0"
}
},
"@jest/source-map": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.5.2.tgz",
- "integrity": "sha512-c5CehvkbfHX6yyNg7bMn2ylqlp9AHXuZ/3hPH/Lh5bxD/vefd/lV9HBjMqXef9yQNqkdwyZvkLSVSwB38hy8Qw==",
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz",
+ "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==",
"dev": true,
"requires": {
- "@jridgewell/trace-mapping": "^0.3.31",
+ "@jridgewell/trace-mapping": "^0.3.25",
"callsites": "^3.1.0",
- "convert-source-map": "^2.0.0",
"graceful-fs": "^4.2.11"
}
},
"@jest/test-result": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.5.2.tgz",
- "integrity": "sha512-eO6YU5rsLfs60ne694e0IAmRgeBnoA8mu0nlxXDbl/1j5km0o2vO9/VbbUIKIH+WRKKEI+ELgwraRGn2Lhep8g==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz",
+ "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==",
"dev": true,
"requires": {
- "@jest/console": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/console": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/istanbul-lib-coverage": "^2.0.6",
"collect-v8-coverage": "^1.0.2"
}
},
"@jest/test-sequencer": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.5.2.tgz",
- "integrity": "sha512-bmij8jZyYAC47ExT2GeP7PcrlwSNS+Bp3KeRuBH88gpcxqDv6fljN8PemS4J/lLZ79xb6mpaennQTTq0zUAVVg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz",
+ "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==",
"dev": true,
"requires": {
- "@jest/test-result": "30.5.2",
+ "@jest/test-result": "30.4.1",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.5.1",
+ "jest-haste-map": "30.4.1",
"slash": "^3.0.0"
}
},
"@jest/transform": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.5.2.tgz",
- "integrity": "sha512-LBGwmaE886C41jDZ65eqR1YbXM7642MK3ZhTowZd5+xDlrDLuf4ePuDdDmVSvcr8NTVJ3MqxLkMbUCzuigUaoQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz",
+ "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==",
"dev": true,
"requires": {
"@babel/core": "^7.27.4",
- "@jest/types": "30.5.1",
- "@jridgewell/trace-mapping": "^0.3.31",
- "babel-plugin-istanbul": "^8.0.0",
+ "@jest/types": "30.4.1",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "babel-plugin-istanbul": "^7.0.1",
"chalk": "^4.1.2",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.5.1",
- "jest-regex-util": "30.5.0",
- "jest-util": "30.5.1",
+ "jest-haste-map": "30.4.1",
+ "jest-regex-util": "30.4.0",
+ "jest-util": "30.4.1",
"pirates": "^4.0.7",
"slash": "^3.0.0",
"write-file-atomic": "^5.0.1"
}
},
"@jest/types": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.1.tgz",
- "integrity": "sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz",
+ "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==",
"dev": true,
"requires": {
- "@jest/pattern": "30.5.0",
- "@jest/schemas": "30.5.0",
+ "@jest/pattern": "30.4.0",
+ "@jest/schemas": "30.4.1",
"@types/istanbul-lib-coverage": "^2.0.6",
"@types/istanbul-reports": "^3.0.4",
"@types/node": "*",
@@ -18789,9 +18696,9 @@
"optional": true
},
"@napi-rs/wasm-runtime": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz",
- "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==",
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
"dev": true,
"optional": true,
"requires": {
@@ -18822,59 +18729,6 @@
"typescript": "5.9.3",
"webpack": "5.106.2",
"webpack-node-externals": "3.0.0"
- },
- "dependencies": {
- "@nestjs/schematics": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz",
- "integrity": "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==",
- "dev": true,
- "requires": {
- "@angular-devkit/core": "19.2.24",
- "@angular-devkit/schematics": "19.2.24",
- "comment-json": "5.0.0",
- "jsonc-parser": "3.3.1",
- "pluralize": "8.0.0"
- },
- "dependencies": {
- "@angular-devkit/core": {
- "version": "19.2.24",
- "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz",
- "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==",
- "dev": true,
- "requires": {
- "ajv": "8.18.0",
- "ajv-formats": "3.0.1",
- "jsonc-parser": "3.3.1",
- "picomatch": "4.0.4",
- "rxjs": "7.8.1",
- "source-map": "0.7.4"
- }
- },
- "@angular-devkit/schematics": {
- "version": "19.2.24",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz",
- "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==",
- "dev": true,
- "requires": {
- "@angular-devkit/core": "19.2.24",
- "jsonc-parser": "3.3.1",
- "magic-string": "0.30.17",
- "ora": "5.4.1",
- "rxjs": "7.8.1"
- }
- }
- }
- },
- "rxjs": {
- "version": "7.8.1",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
- "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
- "dev": true,
- "requires": {
- "tslib": "^2.1.0"
- }
- }
}
},
"@nestjs/common": {
@@ -18970,179 +18824,52 @@
}
},
"@nestjs/schematics": {
- "version": "12.0.5",
- "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-12.0.5.tgz",
- "integrity": "sha512-AuZJVAglqzjFyXVy1EIyzjyEArw6mdOF19vWTgVviJICvVAK8DT97q+6CX4v3wWFhGZoDbGiDnbq6EsGub7/bA==",
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.1.0.tgz",
+ "integrity": "sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==",
"dev": true,
"requires": {
- "@angular-devkit/core": "22.1.8",
- "@angular-devkit/schematics": "22.1.8",
+ "@angular-devkit/core": "19.2.24",
+ "@angular-devkit/schematics": "19.2.24",
"comment-json": "5.0.0",
"jsonc-parser": "3.3.1",
"pluralize": "8.0.0"
},
"dependencies": {
"@angular-devkit/core": {
- "version": "22.1.8",
- "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.1.8.tgz",
- "integrity": "sha512-34lsgg2FwMVBX7nR/ZqzRUcdvdYPvSB8XAEr2GudXAyZwLI9ehzfQlagAWR/gEb8p4uCFZW0r7uU0toxydq4Rw==",
+ "version": "19.2.24",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.24.tgz",
+ "integrity": "sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==",
"dev": true,
"requires": {
- "ajv": "8.20.0",
+ "ajv": "8.18.0",
"ajv-formats": "3.0.1",
"jsonc-parser": "3.3.1",
- "picomatch": "4.0.5",
- "rxjs": "7.8.2",
- "source-map": "0.7.6"
+ "picomatch": "4.0.4",
+ "rxjs": "7.8.1",
+ "source-map": "0.7.4"
}
},
"@angular-devkit/schematics": {
- "version": "22.1.8",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.1.8.tgz",
- "integrity": "sha512-Pv3cPa/44kvEwYcqwx4Ns5dhIDmcFNSHhbpheqiEG1Z/u4e2t4zHKDtE3eHZB+8o+IcC7xJj+d+AqGR44RoZDA==",
+ "version": "19.2.24",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.24.tgz",
+ "integrity": "sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==",
"dev": true,
"requires": {
- "@angular-devkit/core": "22.1.8",
+ "@angular-devkit/core": "19.2.24",
"jsonc-parser": "3.3.1",
- "magic-string": "1.0.0",
- "ora": "9.4.1",
- "rxjs": "7.8.2"
- }
- },
- "ajv": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
- "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- }
- },
- "ansi-regex": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz",
- "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==",
- "dev": true
- },
- "chalk": {
- "version": "5.6.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
- "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
- "dev": true
- },
- "cli-cursor": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
- "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
- "dev": true,
- "requires": {
- "restore-cursor": "^5.0.0"
- }
- },
- "cli-spinners": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
- "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==",
- "dev": true
- },
- "is-interactive": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
- "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
- "dev": true
- },
- "is-unicode-supported": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
- "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
- "dev": true
- },
- "log-symbols": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
- "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
- "dev": true,
- "requires": {
- "is-unicode-supported": "^2.0.0",
- "yoctocolors": "^2.1.1"
- }
- },
- "magic-string": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.0.0.tgz",
- "integrity": "sha512-CGvjzMN08iv6w1mm4/x3Gh1hLb4VnyRUA15FFpl6CsCIGGoe36k7kY5KNz9QDbSBN5I/fWHM6ZlIkUTa5xdUEA==",
- "dev": true,
- "requires": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "onetime": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
- "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
- "dev": true,
- "requires": {
- "mimic-function": "^5.0.0"
- }
- },
- "ora": {
- "version": "9.4.1",
- "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz",
- "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==",
- "dev": true,
- "requires": {
- "chalk": "^5.6.2",
- "cli-cursor": "^5.0.0",
- "cli-spinners": "^3.2.0",
- "is-interactive": "^2.0.0",
- "is-unicode-supported": "^2.1.0",
- "log-symbols": "^7.0.1",
- "stdin-discarder": "^0.3.2",
- "string-width": "^8.1.0"
- }
- },
- "picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "dev": true
- },
- "restore-cursor": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
- "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
- "dev": true,
- "requires": {
- "onetime": "^7.0.0",
- "signal-exit": "^4.1.0"
+ "magic-string": "0.30.17",
+ "ora": "5.4.1",
+ "rxjs": "7.8.1"
}
},
- "source-map": {
- "version": "0.7.6",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
- "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
- "dev": true
- },
- "string-width": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz",
- "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==",
- "dev": true,
- "requires": {
- "get-east-asian-width": "^1.5.0",
- "strip-ansi": "^7.1.2"
- }
- },
- "strip-ansi": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
- "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
"requires": {
- "ansi-regex": "^6.2.2"
+ "tslib": "^2.1.0"
}
}
}
@@ -20062,127 +19789,19 @@
"@opentelemetry/sql-common": {
"version": "0.42.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.42.0.tgz",
- "integrity": "sha512-nwUwUU+8O8a4bnLqk6CodWeegGMEANgC94KTAhXcpGWLrW/2/hek/0ajNbjXnSOoNuCX+nteUPs46HFHhou9Xw==",
- "requires": {
- "@opentelemetry/core": "^2.0.0"
- }
- },
- "@paralleldrive/cuid2": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
- "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
- "dev": true,
- "requires": {
- "@noble/hashes": "^1.1.5"
- }
- },
- "@parcel/watcher": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz",
- "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==",
- "dev": true,
- "requires": {
- "@parcel/watcher-android-arm64": "2.6.0",
- "@parcel/watcher-darwin-arm64": "2.6.0",
- "@parcel/watcher-darwin-x64": "2.6.0",
- "@parcel/watcher-freebsd-x64": "2.6.0",
- "@parcel/watcher-linux-arm-glibc": "2.6.0",
- "@parcel/watcher-linux-arm-musl": "2.6.0",
- "@parcel/watcher-linux-arm64-glibc": "2.6.0",
- "@parcel/watcher-linux-arm64-musl": "2.6.0",
- "@parcel/watcher-linux-x64-glibc": "2.6.0",
- "@parcel/watcher-linux-x64-musl": "2.6.0",
- "@parcel/watcher-win32-arm64": "2.6.0",
- "@parcel/watcher-win32-x64": "2.6.0",
- "detect-libc": "^2.0.3",
- "is-glob": "^4.0.3",
- "node-addon-api": "^7.0.0",
- "picomatch": "^4.0.4"
- }
- },
- "@parcel/watcher-android-arm64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz",
- "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==",
- "dev": true,
- "optional": true
- },
- "@parcel/watcher-darwin-arm64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz",
- "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==",
- "dev": true,
- "optional": true
- },
- "@parcel/watcher-darwin-x64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz",
- "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==",
- "dev": true,
- "optional": true
- },
- "@parcel/watcher-freebsd-x64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz",
- "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==",
- "dev": true,
- "optional": true
- },
- "@parcel/watcher-linux-arm-glibc": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz",
- "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==",
- "dev": true,
- "optional": true
- },
- "@parcel/watcher-linux-arm-musl": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz",
- "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==",
- "dev": true,
- "optional": true
- },
- "@parcel/watcher-linux-arm64-glibc": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz",
- "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==",
- "dev": true,
- "optional": true
- },
- "@parcel/watcher-linux-arm64-musl": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz",
- "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==",
- "dev": true,
- "optional": true
- },
- "@parcel/watcher-linux-x64-glibc": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz",
- "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==",
- "dev": true,
- "optional": true
- },
- "@parcel/watcher-linux-x64-musl": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz",
- "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==",
- "dev": true,
- "optional": true
- },
- "@parcel/watcher-win32-arm64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz",
- "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==",
- "dev": true,
- "optional": true
+ "integrity": "sha512-nwUwUU+8O8a4bnLqk6CodWeegGMEANgC94KTAhXcpGWLrW/2/hek/0ajNbjXnSOoNuCX+nteUPs46HFHhou9Xw==",
+ "requires": {
+ "@opentelemetry/core": "^2.0.0"
+ }
},
- "@parcel/watcher-win32-x64": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz",
- "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==",
+ "@paralleldrive/cuid2": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
+ "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
"dev": true,
- "optional": true
+ "requires": {
+ "@noble/hashes": "^1.1.5"
+ }
},
"@pkgjs/parseargs": {
"version": "0.11.0",
@@ -20511,6 +20130,63 @@
"@sinonjs/commons": "^3.0.1"
}
},
+ "@smithy/core": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.1.tgz",
+ "integrity": "sha512-i4YPS4B6ts7bjn7UwLnGjiZdprOvHvgGobFZsYK3GIY3E5hIqtj0rReU69BcTpGp+fvtraSNXeG1l+jtJvF55w==",
+ "requires": {
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@smithy/credential-provider-imds": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz",
+ "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==",
+ "requires": {
+ "@smithy/core": "^3.33.2",
+ "@smithy/types": "^4.17.2",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@smithy/fetch-http-handler": {
+ "version": "5.8.0",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz",
+ "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==",
+ "requires": {
+ "@smithy/core": "^3.33.3",
+ "@smithy/types": "^4.18.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@smithy/node-http-handler": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz",
+ "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==",
+ "requires": {
+ "@smithy/core": "^3.33.3",
+ "@smithy/types": "^4.18.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@smithy/signature-v4": {
+ "version": "5.7.4",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz",
+ "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==",
+ "requires": {
+ "@smithy/core": "^3.35.0",
+ "@smithy/types": "^4.19.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "@smithy/types": {
+ "version": "4.19.0",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz",
+ "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==",
+ "requires": {
+ "tslib": "^2.6.2"
+ }
+ },
"@so-ric/colorspace": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
@@ -21013,9 +20689,9 @@
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
},
"@tybys/wasm-util": {
- "version": "0.10.4",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
- "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==",
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"dev": true,
"optional": true,
"requires": {
@@ -21256,20 +20932,18 @@
}
},
"@types/node": {
- "version": "20.19.43",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
- "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
- "requires": {
- "undici-types": "~6.21.0"
- }
- },
- "@types/node-int64": {
- "version": "0.4.32",
- "resolved": "https://registry.npmjs.org/@types/node-int64/-/node-int64-0.4.32.tgz",
- "integrity": "sha512-xf/JsSlnXQ+mzvc0IpXemcrO4BrCfpgNpMco+GLcXkFk01k/gW9lGJu+Vof0ZSvHK6DsHJDPSbjFPs36QkWXqw==",
- "dev": true,
+ "version": "26.6.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz",
+ "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==",
"requires": {
- "@types/node": "*"
+ "undici-types": "~8.9.0"
+ },
+ "dependencies": {
+ "undici-types": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
+ "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="
+ }
}
},
"@types/node-int64": {
@@ -22079,37 +21753,37 @@
}
},
"babel-jest": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.2.tgz",
- "integrity": "sha512-ES8WeJ2fNWPEDunyAtUfJ12nHyaWUloCHdkfK2oW5uSoQSUmjNKdWiGaXJITJwvFEWoD/evHEg/QoCXAvLaQqQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz",
+ "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==",
"dev": true,
"requires": {
- "@jest/transform": "30.5.2",
+ "@jest/transform": "30.4.1",
"@types/babel__core": "^7.20.5",
- "babel-plugin-istanbul": "^8.0.0",
- "babel-preset-jest": "30.5.0",
+ "babel-plugin-istanbul": "^7.0.1",
+ "babel-preset-jest": "30.4.0",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"slash": "^3.0.0"
}
},
"babel-plugin-istanbul": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-8.0.0.tgz",
- "integrity": "sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz",
+ "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.0.0",
"@istanbuljs/load-nyc-config": "^1.0.0",
"@istanbuljs/schema": "^0.1.3",
"istanbul-lib-instrument": "^6.0.2",
- "test-exclude": "^7.0.1"
+ "test-exclude": "^6.0.0"
}
},
"babel-plugin-jest-hoist": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.5.0.tgz",
- "integrity": "sha512-gtGo1B+u14jrZQv6TdSWIWkTqclboo7Qn+dFAGUOIuXLFuJKAdg3U5MIWzZnW4vfUb4dX9skmAMiby86e/SF4A==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz",
+ "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==",
"dev": true,
"requires": {
"@types/babel__core": "^7.20.5"
@@ -22139,12 +21813,12 @@
}
},
"babel-preset-jest": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.5.0.tgz",
- "integrity": "sha512-ZGPn5ClP4lBDpuOK8W1yQIOy359HmbnZv3suucAlIe+SEE9yDsxV4S2PjSbH2Vc97U+WmzYD7vw3kr8NsQ/i6w==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz",
+ "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==",
"dev": true,
"requires": {
- "babel-plugin-jest-hoist": "30.5.0",
+ "babel-plugin-jest-hoist": "30.4.0",
"babel-preset-current-node-syntax": "^1.2.0"
}
},
@@ -22256,6 +21930,11 @@
}
}
},
+ "bowser": {
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
+ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="
+ },
"brace-expansion": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
@@ -22873,7 +22552,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "devOptional": true
+ "optional": true
},
"detect-newline": {
"version": "3.1.0",
@@ -23311,17 +22990,17 @@
"dev": true
},
"expect": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.2.tgz",
- "integrity": "sha512-0LNuMvs8/5mRYhnCR4k+lGV7fqPMs6Bnksda4S3zsCUmdxBpAn4zU4NNzXdq1pMKe/H4W5t/zVIDSJ/H3e0vDA==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz",
+ "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==",
"dev": true,
"requires": {
- "@jest/expect-utils": "30.5.2",
- "@jest/get-type": "30.5.0",
- "jest-matcher-utils": "30.5.2",
- "jest-message-util": "30.5.1",
- "jest-mock": "30.5.2",
- "jest-util": "30.5.1"
+ "@jest/expect-utils": "30.4.1",
+ "@jest/get-type": "30.1.0",
+ "jest-matcher-utils": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-mock": "30.4.1",
+ "jest-util": "30.4.1"
}
},
"express": {
@@ -23399,6 +23078,14 @@
"dev": true,
"requires": {
"pure-rand": "^8.0.0"
+ },
+ "dependencies": {
+ "pure-rand": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz",
+ "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==",
+ "dev": true
+ }
}
},
"fast-deep-equal": {
@@ -23472,13 +23159,6 @@
"bser": "2.1.1"
}
},
- "fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "requires": {}
- },
"fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
@@ -24379,330 +24059,391 @@
}
},
"jest": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.2.tgz",
- "integrity": "sha512-3gnpdBIza8ijCl3iMV9ChE3hSt1+46865qqod863gQtJr1DixOkAU5DoGbi3u+XlRcw++BhZQMu/y4xBdHBvSQ==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz",
+ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==",
"dev": true,
"requires": {
- "@jest/core": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/core": "30.4.2",
+ "@jest/types": "30.4.1",
"import-local": "^3.2.0",
- "jest-cli": "30.5.2"
+ "jest-cli": "30.4.2"
}
},
"jest-changed-files": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.5.1.tgz",
- "integrity": "sha512-0+bvMM/ENhDI29Z8q1r4HxiDIi4G5tnBmSw4esfPQoj9q8Nik7KIyuxHkTVnnniJQf05SxpGaKDQ+4h28ynGPg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz",
+ "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==",
"dev": true,
"requires": {
"execa": "^5.1.1",
- "jest-util": "30.5.1",
+ "jest-util": "30.4.1",
"p-limit": "^3.1.0"
}
},
"jest-circus": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.2.tgz",
- "integrity": "sha512-vffFgjs5JSJ9q4ds1vNW3klKYPJfYfIY61LT/zaZgvTeASJOWZUt8LBfHgIcV4rwxlPBLl/ePBGccHEclS4PlQ==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz",
+ "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==",
"dev": true,
"requires": {
- "@jest/environment": "30.5.2",
- "@jest/expect": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/environment": "30.4.1",
+ "@jest/expect": "30.4.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"chalk": "^4.1.2",
"co": "^4.6.0",
"dedent": "^1.6.0",
"is-generator-fn": "^2.1.0",
- "jest-each": "30.5.2",
- "jest-matcher-utils": "30.5.2",
- "jest-message-util": "30.5.1",
- "jest-runtime": "30.5.2",
- "jest-snapshot": "30.5.2",
- "jest-util": "30.5.1",
+ "jest-each": "30.4.1",
+ "jest-matcher-utils": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-runtime": "30.4.2",
+ "jest-snapshot": "30.4.1",
+ "jest-util": "30.4.1",
"p-limit": "^3.1.0",
- "pretty-format": "30.5.1",
+ "pretty-format": "30.4.1",
"pure-rand": "^7.0.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
- },
- "dependencies": {
- "pure-rand": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
- "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==",
- "dev": true
- }
}
},
"jest-cli": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.2.tgz",
- "integrity": "sha512-YYYxUUVjFgPvgEleKNKMaYGIFgR3l3832Cl0kRL/oCiDZ03v7wImUtquHl2OOBCCNgprjZgBN5bczS/JklPBDg==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz",
+ "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==",
"dev": true,
"requires": {
- "@jest/core": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/core": "30.4.2",
+ "@jest/test-result": "30.4.1",
+ "@jest/types": "30.4.1",
"chalk": "^4.1.2",
"exit-x": "^0.2.2",
"import-local": "^3.2.0",
- "jest-config": "30.5.2",
- "jest-util": "30.5.1",
- "jest-validate": "30.5.1",
+ "jest-config": "30.4.2",
+ "jest-util": "30.4.1",
+ "jest-validate": "30.4.1",
"yargs": "^17.7.2"
}
},
"jest-config": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.2.tgz",
- "integrity": "sha512-U6221OZekabePvGcGPf4raIBKMvYQWFHvhobDJRojOYaFMj2HaGytAdknjDhYc6JTz2fdHW9+6k0wWUDaNeOFQ==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz",
+ "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==",
"dev": true,
"requires": {
"@babel/core": "^7.27.4",
- "@jest/get-type": "30.5.0",
- "@jest/pattern": "30.5.0",
- "@jest/test-sequencer": "30.5.2",
- "@jest/types": "30.5.1",
- "babel-jest": "30.5.2",
+ "@jest/get-type": "30.1.0",
+ "@jest/pattern": "30.4.0",
+ "@jest/test-sequencer": "30.4.1",
+ "@jest/types": "30.4.1",
+ "babel-jest": "30.4.1",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"deepmerge": "^4.3.1",
- "glob": "^13.0.6",
+ "glob": "^10.5.0",
"graceful-fs": "^4.2.11",
- "jest-circus": "30.5.2",
- "jest-docblock": "30.5.0",
- "jest-environment-node": "30.5.2",
- "jest-regex-util": "30.5.0",
- "jest-resolve": "30.5.1",
- "jest-runner": "30.5.2",
- "jest-util": "30.5.1",
- "jest-validate": "30.5.1",
+ "jest-circus": "30.4.2",
+ "jest-docblock": "30.4.0",
+ "jest-environment-node": "30.4.1",
+ "jest-regex-util": "30.4.0",
+ "jest-resolve": "30.4.1",
+ "jest-runner": "30.4.2",
+ "jest-util": "30.4.1",
+ "jest-validate": "30.4.1",
"parse-json": "^5.2.0",
- "pretty-format": "30.5.1",
+ "pretty-format": "30.4.1",
"slash": "^3.0.0",
"strip-json-comments": "^3.1.1"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "dev": true,
+ "requires": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ }
+ },
+ "lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
+ "path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ }
+ }
}
},
"jest-diff": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.2.tgz",
- "integrity": "sha512-rBtHnI6BWTiWj3lM7fOLVfChx7R9B0u9VV3PRHawKs79x6ZjW1QoSrKenaigNGWlvhtoi2BqvDbABDie+Os3xQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz",
+ "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==",
"dev": true,
"requires": {
- "@jest/diff-sequences": "30.5.0",
- "@jest/get-type": "30.5.0",
+ "@jest/diff-sequences": "30.4.0",
+ "@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
- "pretty-format": "30.5.1"
+ "pretty-format": "30.4.1"
}
},
"jest-docblock": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.5.0.tgz",
- "integrity": "sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz",
+ "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==",
"dev": true,
"requires": {
"detect-newline": "^3.1.0"
}
},
"jest-each": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.2.tgz",
- "integrity": "sha512-GRrW+7fmmgmkvyfEBqc1QFdWnF1Ex5yG6Y6AWL4TTxnkWlKIT98dsKh3P3UuWm6v4XsQwVE6DEzXIPMzWJ3ZbA==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz",
+ "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==",
"dev": true,
"requires": {
- "@jest/get-type": "30.5.0",
- "@jest/types": "30.5.1",
+ "@jest/get-type": "30.1.0",
+ "@jest/types": "30.4.1",
"chalk": "^4.1.2",
- "jest-regex-util": "30.5.0",
- "jest-util": "30.5.1",
- "pretty-format": "30.5.1"
+ "jest-util": "30.4.1",
+ "pretty-format": "30.4.1"
}
},
"jest-environment-node": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.2.tgz",
- "integrity": "sha512-Ciz4KgTGIbojxSKpuFImgWJj5EyqfE8xMq9NxWmOXnQywHHS03H9Mv+vaCB29J5pr3JBrY27WbRV18rBdjh8Dg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz",
+ "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==",
"dev": true,
"requires": {
- "@jest/environment": "30.5.2",
- "@jest/fake-timers": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/environment": "30.4.1",
+ "@jest/fake-timers": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
- "jest-mock": "30.5.2",
- "jest-util": "30.5.1",
- "jest-validate": "30.5.1"
+ "jest-mock": "30.4.1",
+ "jest-util": "30.4.1",
+ "jest-validate": "30.4.1"
}
},
"jest-haste-map": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.5.1.tgz",
- "integrity": "sha512-VIFgt67jW480YDxKfEv9IYQKrFpYt7bOCMn3VsnjK7AK9qo7p6acpnA1DHwsVOULE3dTaYew/6JaTD/d0VDHoQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz",
+ "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==",
"dev": true,
"requires": {
- "@jest/types": "30.5.1",
- "@parcel/watcher": "^2.6.0",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
- "fdir": "^6.5.0",
+ "fsevents": "^2.3.3",
"graceful-fs": "^4.2.11",
- "jest-regex-util": "30.5.0",
- "jest-util": "30.5.1",
- "jest-worker": "30.5.1",
- "picomatch": "^4.0.3"
+ "jest-regex-util": "30.4.0",
+ "jest-util": "30.4.1",
+ "jest-worker": "30.4.1",
+ "picomatch": "^4.0.3",
+ "walker": "^1.0.8"
}
},
"jest-leak-detector": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.5.1.tgz",
- "integrity": "sha512-gt4GT2aWgEoCNTcBe4rqS74xTIUJxi+UD9SNSu9aOk5LmTEd6fS5KSTmAKAfitCCktQHNN+upUuL6EkBfFbMDQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz",
+ "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==",
"dev": true,
"requires": {
- "@jest/get-type": "30.5.0",
- "pretty-format": "30.5.1"
+ "@jest/get-type": "30.1.0",
+ "pretty-format": "30.4.1"
}
},
"jest-matcher-utils": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.2.tgz",
- "integrity": "sha512-lFkB365PTIHOhPzwrb9T6gvlDnPpOZ7/c3ewOKKB7bzztqjVrtlyRL2MkyfJXpyY2u2SKw001JKrTx14fBUFcQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz",
+ "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==",
"dev": true,
"requires": {
- "@jest/get-type": "30.5.0",
+ "@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
- "jest-diff": "30.5.2",
- "pretty-format": "30.5.1"
+ "jest-diff": "30.4.1",
+ "pretty-format": "30.4.1"
}
},
"jest-message-util": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.1.tgz",
- "integrity": "sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz",
+ "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.27.1",
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
- "jest-util": "30.5.1",
+ "jest-util": "30.4.1",
"picomatch": "^4.0.3",
- "pretty-format": "30.5.1",
+ "pretty-format": "30.4.1",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
}
},
"jest-mock": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.2.tgz",
- "integrity": "sha512-KTHb37Fhj8xY8qPFvTFaFJHcdsumQp+ExsUlgoX232Agmc706gagBs53+CYvulV7MVQo9AgXOUxsqzzVBoJlrA==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz",
+ "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==",
"dev": true,
"requires": {
- "@jest/expect-utils": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
- "jest-util": "30.5.1"
+ "jest-util": "30.4.1"
}
},
+ "jest-pnp-resolver": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+ "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
+ "dev": true,
+ "requires": {}
+ },
"jest-regex-util": {
- "version": "30.5.0",
- "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.5.0.tgz",
- "integrity": "sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==",
+ "version": "30.4.0",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz",
+ "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==",
"dev": true
},
"jest-resolve": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.5.1.tgz",
- "integrity": "sha512-wprhLejRtwN6h8ZgaqC0eYjGJ1uMdGPT/+b3eFncbG4NWuuP9QL+vWwRinu9waw7hkOLcpMJGVJAMgBwsPssMQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz",
+ "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==",
"dev": true,
"requires": {
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.5.1",
- "jest-util": "30.5.1",
- "jest-validate": "30.5.1",
+ "jest-haste-map": "30.4.1",
+ "jest-pnp-resolver": "^1.2.3",
+ "jest-util": "30.4.1",
+ "jest-validate": "30.4.1",
"slash": "^3.0.0",
- "unrs-resolver": "^1.12.1"
+ "unrs-resolver": "^1.7.11"
}
},
"jest-resolve-dependencies": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.2.tgz",
- "integrity": "sha512-GMI0DP5enX9Z2qW2zFy1bOkrCfWAOPRJQ7qHt0pdfrxBPsgWxglMPrRSK2TX/wRWYxcPRSLFg3Z88qPOzHx7FQ==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz",
+ "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==",
"dev": true,
"requires": {
- "jest-regex-util": "30.5.0",
- "jest-snapshot": "30.5.2"
+ "jest-regex-util": "30.4.0",
+ "jest-snapshot": "30.4.1"
}
},
"jest-runner": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.2.tgz",
- "integrity": "sha512-/B6px7hiiNhVSRwuv9AGn0nawYvwsHeQZ3ItTd5Gp2diFd0EOKaWVeWqcbw3L88vx1GfpCKjQkxWItZCTl11Rw==",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz",
+ "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==",
"dev": true,
"requires": {
- "@jest/console": "30.5.2",
- "@jest/environment": "30.5.2",
- "@jest/source-map": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/transform": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/console": "30.4.1",
+ "@jest/environment": "30.4.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/transform": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"chalk": "^4.1.2",
"emittery": "^0.13.1",
"exit-x": "^0.2.2",
"graceful-fs": "^4.2.11",
- "jest-docblock": "30.5.0",
- "jest-environment-node": "30.5.2",
- "jest-haste-map": "30.5.1",
- "jest-leak-detector": "30.5.1",
- "jest-message-util": "30.5.1",
- "jest-resolve": "30.5.1",
- "jest-runtime": "30.5.2",
- "jest-util": "30.5.1",
- "jest-watcher": "30.5.2",
- "jest-worker": "30.5.1",
- "p-limit": "^3.1.0"
+ "jest-docblock": "30.4.0",
+ "jest-environment-node": "30.4.1",
+ "jest-haste-map": "30.4.1",
+ "jest-leak-detector": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-resolve": "30.4.1",
+ "jest-runtime": "30.4.2",
+ "jest-util": "30.4.1",
+ "jest-watcher": "30.4.1",
+ "jest-worker": "30.4.1",
+ "p-limit": "^3.1.0",
+ "source-map-support": "0.5.13"
}
},
"jest-runtime": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.2.tgz",
- "integrity": "sha512-0paUAHBlheai10A3XSy9Xh+YBG0IROy+/qzcDPzQ/nbbXrznQzeWy5qniICWK6d3W3scaRQCVAbXdcK0ibBq4w==",
- "dev": true,
- "requires": {
- "@jest/environment": "30.5.2",
- "@jest/fake-timers": "30.5.2",
- "@jest/globals": "30.5.2",
- "@jest/source-map": "30.5.2",
- "@jest/test-result": "30.5.2",
- "@jest/transform": "30.5.2",
- "@jest/types": "30.5.1",
+ "version": "30.4.2",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz",
+ "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==",
+ "dev": true,
+ "requires": {
+ "@jest/environment": "30.4.1",
+ "@jest/fake-timers": "30.4.1",
+ "@jest/globals": "30.4.1",
+ "@jest/source-map": "30.0.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/transform": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"chalk": "^4.1.2",
- "cjs-module-lexer": "^2.2.0",
+ "cjs-module-lexer": "^2.1.0",
"collect-v8-coverage": "^1.0.2",
- "es-module-lexer": "^2.1.0",
- "glob": "^13.0.6",
+ "glob": "^10.5.0",
"graceful-fs": "^4.2.11",
- "jest-haste-map": "30.5.1",
- "jest-message-util": "30.5.1",
- "jest-mock": "30.5.2",
- "jest-regex-util": "30.5.0",
- "jest-resolve": "30.5.1",
- "jest-snapshot": "30.5.2",
- "jest-util": "30.5.1",
+ "jest-haste-map": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-mock": "30.4.1",
+ "jest-regex-util": "30.4.0",
+ "jest-resolve": "30.4.1",
+ "jest-snapshot": "30.4.1",
+ "jest-util": "30.4.1",
"slash": "^3.0.0",
"strip-bom": "^4.0.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "dev": true,
+ "requires": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ }
+ },
+ "lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
+ "path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ }
+ }
}
},
"jest-snapshot": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.2.tgz",
- "integrity": "sha512-jKIdes25AnAH73dOCi2qE6J6vHJ6L2vyBS11M89kT50DVJcLTf2AOjvcc+2vVaN8lUbY5v2R83aqXo3Ro9fqYA==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz",
+ "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==",
"dev": true,
"requires": {
"@babel/core": "^7.27.4",
@@ -24710,31 +24451,31 @@
"@babel/plugin-syntax-jsx": "^7.27.1",
"@babel/plugin-syntax-typescript": "^7.27.1",
"@babel/types": "^7.27.3",
- "@jest/expect-utils": "30.5.2",
- "@jest/get-type": "30.5.0",
- "@jest/snapshot-utils": "30.5.1",
- "@jest/transform": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/expect-utils": "30.4.1",
+ "@jest/get-type": "30.1.0",
+ "@jest/snapshot-utils": "30.4.1",
+ "@jest/transform": "30.4.1",
+ "@jest/types": "30.4.1",
"babel-preset-current-node-syntax": "^1.2.0",
"chalk": "^4.1.2",
- "expect": "30.5.2",
+ "expect": "30.4.1",
"graceful-fs": "^4.2.11",
- "jest-diff": "30.5.2",
- "jest-matcher-utils": "30.5.2",
- "jest-message-util": "30.5.1",
- "jest-util": "30.5.1",
- "pretty-format": "30.5.1",
+ "jest-diff": "30.4.1",
+ "jest-matcher-utils": "30.4.1",
+ "jest-message-util": "30.4.1",
+ "jest-util": "30.4.1",
+ "pretty-format": "30.4.1",
"semver": "^7.7.2",
"synckit": "^0.11.8"
}
},
"jest-util": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.1.tgz",
- "integrity": "sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz",
+ "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==",
"dev": true,
"requires": {
- "@jest/types": "30.5.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
@@ -24743,17 +24484,17 @@
}
},
"jest-validate": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.5.1.tgz",
- "integrity": "sha512-i/buJ56wTpxihE93hQYNMfdOThS87+HGvLZzZJmU4xggPcdTYQq051iwALLCHp3q+SkCGH+EFejQZz5EbBSkkg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz",
+ "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==",
"dev": true,
"requires": {
- "@jest/get-type": "30.5.0",
- "@jest/types": "30.5.1",
+ "@jest/get-type": "30.1.0",
+ "@jest/types": "30.4.1",
"camelcase": "^6.3.0",
"chalk": "^4.1.2",
"leven": "^3.1.0",
- "pretty-format": "30.5.1"
+ "pretty-format": "30.4.1"
},
"dependencies": {
"camelcase": {
@@ -24765,30 +24506,30 @@
}
},
"jest-watcher": {
- "version": "30.5.2",
- "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.2.tgz",
- "integrity": "sha512-rqRgg4R11GA9m1UsEZwQeixHsgZReCn6TOHAHtIH33yXrIvoX0OGPA20Yzzjkz7X40fZvcRSxEpQppYPcWzmcQ==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz",
+ "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==",
"dev": true,
"requires": {
- "@jest/test-result": "30.5.2",
- "@jest/types": "30.5.1",
+ "@jest/test-result": "30.4.1",
+ "@jest/types": "30.4.1",
"@types/node": "*",
"ansi-escapes": "^4.3.2",
"chalk": "^4.1.2",
"emittery": "^0.13.1",
- "jest-util": "30.5.1",
+ "jest-util": "30.4.1",
"string-length": "^4.0.2"
}
},
"jest-worker": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.5.1.tgz",
- "integrity": "sha512-Cbxh5v7AoLuFRmFJSM4/aHdQ68rjXvUWr716EE0Dh3I7T+T/3FgFKhOERGXHcU2Meftq9+9zxPM3TSNyI9D+HA==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz",
+ "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==",
"dev": true,
"requires": {
"@types/node": "*",
"@ungap/structured-clone": "^1.3.0",
- "jest-util": "30.5.1",
+ "jest-util": "30.4.1",
"merge-stream": "^2.0.0",
"supports-color": "^8.1.1"
},
@@ -25139,6 +24880,15 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
+ "makeerror": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
+ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
+ "dev": true,
+ "requires": {
+ "tmpl": "1.0.5"
+ }
+ },
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -25235,12 +24985,6 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
- "mimic-function": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
- "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
- "dev": true
- },
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -25394,12 +25138,6 @@
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="
},
- "node-addon-api": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
- "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
- "dev": true
- },
"node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -25740,12 +25478,14 @@
},
"pg-cloudflare": {
"version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
+ "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
"optional": true
},
"pg-connection-string": {
"version": "2.14.0",
- "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
- "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg=="
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
+ "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="
},
"pg-int8": {
"version": "1.0.1",
@@ -25890,15 +25630,15 @@
"dev": true
},
"pretty-format": {
- "version": "30.5.1",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz",
- "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==",
+ "version": "30.4.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz",
+ "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==",
"dev": true,
"requires": {
- "@jest/react-is-18": "npm:react-is@^18.3.1",
- "@jest/react-is-19": "npm:react-is@^19.2.5",
- "@jest/schemas": "30.5.0",
- "ansi-styles": "^5.2.0"
+ "@jest/schemas": "30.4.1",
+ "ansi-styles": "^5.2.0",
+ "react-is-18": "npm:react-is@^18.3.1",
+ "react-is-19": "npm:react-is@^19.2.5"
},
"dependencies": {
"ansi-styles": {
@@ -25982,9 +25722,9 @@
"dev": true
},
"pure-rand": {
- "version": "8.4.2",
- "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz",
- "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
+ "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==",
"dev": true
},
"q": {
@@ -26031,6 +25771,18 @@
"unpipe": "~1.0.0"
}
},
+ "react-is-18": {
+ "version": "npm:react-is@18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true
+ },
+ "react-is-19": {
+ "version": "npm:react-is@19.2.8",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz",
+ "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==",
+ "dev": true
+ },
"readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -26422,6 +26174,24 @@
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"dev": true
},
+ "source-map-support": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
+ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
"split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -26465,12 +26235,6 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="
},
- "stdin-discarder": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz",
- "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==",
- "dev": true
- },
"streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -26727,81 +26491,47 @@
}
},
"test-exclude": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
- "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
"dev": true,
"requires": {
"@istanbuljs/schema": "^0.1.2",
- "glob": "^10.4.1",
- "minimatch": "^10.2.2"
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
},
"dependencies": {
- "balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true
- },
- "glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "brace-expansion": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"requires": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "dependencies": {
- "minimatch": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
- "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
- "dev": true,
- "requires": {
- "brace-expansion": "^2.0.2"
- }
- }
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
}
},
- "lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true
- },
- "minimatch": {
- "version": "10.2.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
- "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
- "brace-expansion": "^5.0.8"
- },
- "dependencies": {
- "brace-expansion": {
- "version": "5.0.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
- "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
- "dev": true,
- "requires": {
- "balanced-match": "^4.0.2"
- }
- }
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
}
},
- "path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"requires": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ "brace-expansion": "^1.1.7"
}
}
}
@@ -26854,6 +26584,12 @@
"os-tmpdir": "~1.0.2"
}
},
+ "tmpl": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
+ "dev": true
+ },
"to-buffer": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
@@ -27105,7 +26841,8 @@
"undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true
},
"unicorn-magic": {
"version": "0.3.0",
@@ -27250,6 +26987,15 @@
}
}
},
+ "walker": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
+ "dev": true,
+ "requires": {
+ "makeerror": "1.0.12"
+ }
+ },
"watchpack": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz",
diff --git a/package.json b/package.json
index 86826ea8..b899dae4 100644
--- a/package.json
+++ b/package.json
@@ -1,87 +1,6 @@
{
"name": "vortex-backend",
"version": "0.1.0",
- "description": "Vortex cross-chain swap relay backend",
- "author": "Vortex Protocol",
- "private": true,
- "license": "MIT",
- "scripts": {
- "build": "nest build",
- "start": "node dist/main",
- "start:dev": "nest start --watch",
- "dev": "nest start --watch",
- "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
- "typecheck": "tsc --noEmit",
- "test": "jest --config jest.config.js --run",
- "test:e2e": "jest --config jest.e2e.config.js --run",
- "test:watch": "jest --watch",
- "test:cov": "jest --coverage",
- "db:generate": "prisma generate",
- "db:migrate": "prisma migrate dev",
- "db:migrate:deploy": "prisma migrate deploy",
- "db:studio": "prisma studio"
- },
- "dependencies": {
- "@aws-sdk/client-s3": "^3.654.0",
- "@bull-board/api": "^5.22.0",
- "@bull-board/express": "^5.22.0",
- "@msgpack/msgpack": "^3.0.0",
- "@nestjs/common": "^10.3.0",
- "@nestjs/config": "^3.2.0",
- "@nestjs/core": "^10.3.0",
- "@nestjs/platform-express": "^10.3.0",
- "@nestjs/platform-ws": "^10.3.0",
- "@nestjs/schedule": "^4.0.0",
- "@nestjs/swagger": "^7.3.0",
- "@nestjs/throttler": "^5.1.1",
- "@openfeature/server-sdk": "^1.7.5",
- "@opentelemetry/api": "^1.9.0",
- "@opentelemetry/auto-instrumentations-node": "^0.50.0",
- "@opentelemetry/core": "^1.26.0",
- "@opentelemetry/exporter-trace-otlp-proto": "^0.53.0",
- "@opentelemetry/sdk-node": "^0.53.0",
- "@opentelemetry/sdk-trace-base": "^1.26.0",
- "@opentelemetry/sdk-trace-node": "^1.26.0",
- "@prisma/client": "^5.18.0",
- "@sentry/node": "^8.33.0",
- "@stellar/stellar-sdk": "^12.3.0",
- "bullmq": "^5.12.0",
- "class-transformer": "^0.5.1",
- "class-validator": "^0.14.1",
- "express": "^4.21.0",
- "helmet": "^7.1.0",
- "ioredis": "^5.4.1",
- "joi": "^17.13.3",
- "parquetjs": "npm:@dsnp/parquetjs@^1.5.0",
- "pg": "^8.13.0",
- "reflect-metadata": "^0.2.2",
- "rxjs": "^7.8.1",
- "uuid": "^10.0.0",
- "viem": "^2.21.0",
- "winston": "^3.14.2",
- "ws": "^8.18.0",
- "yaml": "^2.5.1",
- "zod": "^3.23.8"
- },
- "devDependencies": {
- "@nestjs/schematics": "^10.1.4",
- "@nestjs/testing": "^10.3.0",
- "@types/express": "^4.17.21",
- "@types/jest": "^29.5.13",
- "@types/node": "^20.16.0",
- "@types/pg": "^8.11.10",
- "@types/supertest": "^6.0.2",
- "@types/ws": "^8.5.12",
- "@typescript-eslint/eslint-plugin": "^7.18.0",
- "@typescript-eslint/parser": "^7.18.0",
- "eslint": "^8.57.1",
- "jest": "^29.7.0",
- "prisma": "^5.18.0",
- "supertest": "^7.0.0",
- "ts-jest": "^29.2.5",
- "ts-node": "^10.9.2",
- "tsconfig-paths": "^4.2.0",
- "typescript": "^5.5.4"
"private": true,
"workspaces": [
"packages/*"
@@ -105,6 +24,7 @@
"check:env-drift": "tsx scripts/check-env-drift.ts",
"seed": "tsx scripts/seed.ts",
"solver:demo": "tsx scripts/solver-bot.ts",
+ "simulate": "tsx tools/simulator/cli.ts",
"canary": "tsx tools/canary/canary.ts",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
@@ -116,6 +36,7 @@
"prepare": "husky"
},
"dependencies": {
+ "@aws-sdk/client-s3": "^3.654.0",
"@bull-board/api": "^9.10.1",
"@bull-board/express": "^9.10.1",
"@nestjs/common": "^11.1.28",
@@ -124,7 +45,6 @@
"@nestjs/event-emitter": "^12.0.1",
"@nestjs/platform-express": "^11.1.28",
"@nestjs/platform-ws": "^11.1.28",
- "@nestjs/schedule": "^6.1.3",
"@nestjs/schedule": "^12.0.2",
"@nestjs/swagger": "^11.4.5",
"@nestjs/throttler": "^6.5.0",
@@ -145,7 +65,7 @@
"dotenv": "^16.4.5",
"helmet": "^7.1.0",
"ioredis": "^6.0.0",
- "joi": "^18.2.9",
+ "joi": "^18.2.3",
"parquetjs": "^0.11.2",
"pg": "8.23.0",
"prom-client": "^15.1.3",
@@ -156,35 +76,32 @@
"viem": "^2.44.4",
"winston": "^3.13.0",
"ws": "^8.18.0",
- "zod": "^4.6.5",
- "@msgpack/msgpack": "^3.0.0",
- "joi": "^18.2.9"
+ "zod": "^3.23.8",
+ "@msgpack/msgpack": "^3.0.0"
},
"devDependencies": {
- "@commitlint/cli": "^21.2.3",
- "@commitlint/config-conventional": "^21.2.3",
+ "@commitlint/cli": "^19.8.1",
+ "@commitlint/config-conventional": "^19.8.1",
"@nestjs/cli": "^11.0.24",
"@nestjs/schematics": "^11.1.0",
- "@msgpack/msgpack": "^3.0.0",
- "joi": "^18.2.9"
- "@nestjs/testing": "^12.1.0",
+ "@nestjs/testing": "^11.1.28",
"@stryker-mutator/core": "^8.0.0",
"@stryker-mutator/jest-runner": "^8.0.0",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.25",
"@types/jest": "^30.0.0",
- "@types/node": "^20.14.2",
+ "@types/node": "^26.6.3",
"@types/parquetjs": "^0.10.6",
"@types/supertest": "^7.2.0",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.12",
"@msgpack/msgpack": "^3.0.0",
- "@typescript-eslint/eslint-plugin": "^8.70.1",
+ "@typescript-eslint/eslint-plugin": "^7.13.0",
"@typescript-eslint/parser": "^7.13.0",
"eslint": "^8.57.0",
"fast-check": "^4.10.2",
"husky": "^9.1.7",
- "jest": "^30.5.2",
+ "jest": "^30.4.2",
"openapi-typescript": "^7.8.0",
"prisma": "5.22.0",
"supertest": "^7.2.2",
diff --git a/packages/solver-sdk/src/generated/api-types.ts b/packages/solver-sdk/src/generated/api-types.ts
index 124cdfca..43c18a41 100644
--- a/packages/solver-sdk/src/generated/api-types.ts
+++ b/packages/solver-sdk/src/generated/api-types.ts
@@ -12,14 +12,14 @@
// prettier-ignore
export interface paths {
- "/health": {
+ "/metrics": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["HealthController_check"];
+ get: operations["MetricsController_index"];
put?: never;
post?: never;
delete?: never;
@@ -28,14 +28,15 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/chain/health": {
+ "/ops/killswitch": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["SorobanController_getHealth"];
+ /** Current kill-switch state and propagation health. */
+ get: operations["KillSwitchController_status"];
put?: never;
post?: never;
delete?: never;
@@ -44,62 +45,64 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/chain/ledger": {
+ "/ops/killswitch/pause": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["SorobanController_getLatestLedger"];
+ get?: never;
put?: never;
- post?: never;
+ /** Pause a scope. Effective on every replica within the propagation budget. */
+ post: operations["KillSwitchController_pause"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/chain/network": {
+ "/ops/killswitch/resume/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["SorobanController_getNetwork"];
+ get?: never;
put?: never;
- post?: never;
+ /** Approve a resume. Takes effect once two distinct operators have approved. */
+ post: operations["KillSwitchController_approveResume"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/chain/account/{publicKey}": {
+ "/intents": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["SorobanController_getAccount"];
+ get: operations["IntentsController_list"];
put?: never;
- post?: never;
+ post: operations["IntentsController_create"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/tokens": {
+ "/intents/open": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["TokensController_getTokens"];
+ get: operations["IntentsController_listOpen"];
put?: never;
post?: never;
delete?: never;
@@ -108,14 +111,14 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/tokens/stellar": {
+ "/intents/user/{address}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["TokensController_getStellarTokens"];
+ get: operations["IntentsController_listByUser"];
put?: never;
post?: never;
delete?: never;
@@ -124,30 +127,34 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/intents": {
+ "/intents/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_list"];
+ get: operations["IntentsController_getOne"];
put?: never;
- post: operations["IntentsController_create"];
+ post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/open": {
+ "/intents/{id}/audit": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_listOpen"];
+ /**
+ * Get audit trail for an intent
+ * @description Returns the full state-transition history for an intent ordered oldest-first. Each entry records the state the intent moved into, who triggered it, and why.
+ */
+ get: operations["IntentsController_getAudit"];
put?: never;
post?: never;
delete?: never;
@@ -156,14 +163,14 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/intents/user/{address}": {
+ "/intents/{id}/quote": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_listByUser"];
+ get: operations["IntentsController_getPersistedQuote"];
put?: never;
post?: never;
delete?: never;
@@ -172,75 +179,79 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}": {
+ "/intents/batch": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_getOne"];
+ get?: never;
put?: never;
- post?: never;
+ /**
+ * Batch-fetch current intent records by ID
+ * @description Returns the current record for each supplied intent ID. IDs with no matching record are omitted (not individually 404'd). Capped at 100 IDs.
+ */
+ post: operations["IntentsController_batchLookup"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/audit": {
+ "/intents/batch-create": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
+ get?: never;
+ put?: never;
/**
- * Get audit trail for an intent
- * @description Returns the full state-transition history for an intent ordered oldest-first. Each entry records the state the intent moved into, who triggered it, and why.
+ * Create multiple intents atomically
+ * @description Creates up to 50 intents in a single atomic (all-or-nothing) request. If any item fails validation or exceeds open intent limits, zero intents are created and per-item errors are reported.
*/
- get: operations["IntentsController_getAudit"];
- put?: never;
- post?: never;
+ post: operations["IntentsController_batchCreate"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/quote": {
+ "/intents/{id}/accept": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_getPersistedQuote"];
+ get?: never;
put?: never;
- post?: never;
+ post: operations["IntentsController_accept"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/auction": {
+ "/intents/{id}/fill": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_getAuctionPrice"];
+ get?: never;
put?: never;
- post?: never;
+ post: operations["IntentsController_fill"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/accept": {
+ "/intents/{id}/cancel": {
parameters: {
query?: never;
header?: never;
@@ -249,14 +260,14 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations["IntentsController_accept"];
+ post: operations["IntentsController_cancel"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/fill": {
+ "/intents/{id}/amend": {
parameters: {
query?: never;
header?: never;
@@ -265,14 +276,14 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations["IntentsController_fill"];
+ post: operations["IntentsController_amend"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/cancel": {
+ "/intents/quote": {
parameters: {
query?: never;
header?: never;
@@ -281,14 +292,14 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations["IntentsController_cancel"];
+ post: operations["IntentsController_quote"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/quote": {
+ "/intents/{id}/requote": {
parameters: {
query?: never;
header?: never;
@@ -297,21 +308,42 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations["IntentsController_quote"];
+ /** Re-quote an existing open intent using its stored fields */
+ post: operations["IntentsController_requote"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/solvers": {
+ "/docs/ws": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["SolversController_getLeaderboard"];
+ /**
+ * AsyncAPI specification for the Vortex WebSocket feed
+ * @description Returns the AsyncAPI 2.6 YAML document describing the vortex.v1 subprotocol. Use this with AsyncAPI Studio or SDK generators.
+ */
+ get: operations["WsDocsController_getSpec"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SolversController_getLegacyLeaderboard"];
put?: never;
post: operations["SolversController_register"];
delete?: never;
@@ -320,7 +352,43 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/solvers/{address}": {
+ "/solvers/leaderboard": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Windowed solver leaderboard
+ * @description Returns the ranked solver list for a specific window. This endpoint is intended for recent-performance visibility and does not alter the legacy all-time leaderboard.
+ */
+ get: operations["SolversController_getLeaderboard"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/eligible-intents": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SolversController_getEligibleIntents"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}": {
parameters: {
query?: never;
header?: never;
@@ -336,7 +404,7 @@ export interface paths {
patch: operations["SolversController_updateSolver"];
trace?: never;
};
- "/api/v1/solvers/{address}/stats": {
+ "/solvers/{address}/stats": {
parameters: {
query?: never;
header?: never;
@@ -352,7 +420,55 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/solvers/{address}/deregister": {
+ "/solvers/{address}/slashes": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SolversController_getSlashHistory"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/slashes/{slashId}/dispute": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["SolversController_submitDispute"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/slashes/{slashId}/dispute/resolve": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["SolversController_resolveDispute"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/deregister": {
parameters: {
query?: never;
header?: never;
@@ -368,7 +484,7 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/solvers/{address}/deactivate": {
+ "/solvers/{address}/deactivate": {
parameters: {
query?: never;
header?: never;
@@ -384,7 +500,7 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/solvers/{address}/reactivate": {
+ "/solvers/{address}/reactivate": {
parameters: {
query?: never;
header?: never;
@@ -400,14 +516,15 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/stats": {
+ "/api/v1/admin/griefing": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["StatsController_getStats"];
+ /** List solvers currently under anti-griefing enforcement */
+ get: operations["SolverGriefingController_listEnforced"];
put?: never;
post?: never;
delete?: never;
@@ -416,99 +533,658 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/stats/ws": {
+ "/api/v1/admin/griefing/{address}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["StatsController_getWsStats"];
+ /** Get anti-griefing record for a solver */
+ get: operations["SolverGriefingController_getSolverRecord"];
put?: never;
post?: never;
- delete?: never;
+ /** Reset a solver's anti-griefing state to ok */
+ delete: operations["SolverGriefingController_resetSolver"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
-}
-export type webhooks = Record;
-export interface components {
- schemas: {
- StellarTokenDto: {
- /** @description Stellar contract ID of the token */
- contract: string;
- /** @example XLM */
- symbol: string;
- /** @example Stellar Lumens */
- name: string;
- /** @example 7 */
- decimals: number;
- /** @example 0.1182 */
- priceUSD: number;
+ "/api/v1/admin/griefing/{address}/audit": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- StellarTokensResponseDto: {
- tokens: components["schemas"]["StellarTokenDto"][];
+ /** Get anti-griefing audit log for a solver */
+ get: operations["SolverGriefingController_getSolverAudit"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/griefing/{address}/exclude/{intentId}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- CreateIntentDto: {
- /** @description Stellar or EVM address of the user creating the intent */
- user: string;
- /**
- * @description Source chain the funds are coming from
- * @enum {string}
- */
- srcChain: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
- /** @description Source token contract/address on srcChain */
- srcTokenAddress: string;
- /** @description Source token symbol, e.g. USDC */
- srcTokenSymbol: string;
- /** @description Source token decimals */
- srcTokenDecimals: number;
- /** @description Source amount as a non-negative integer string (base units) */
- srcAmount: string;
- /** @description Destination Stellar token contract */
- dstTokenContract: string;
- /** @description Destination token symbol, e.g. USDC */
- dstTokenSymbol: string;
- /** @description Destination token decimals */
- dstTokenDecimals: number;
- /** @description Minimum acceptable destination amount as an integer string */
- minDstAmount: string;
- /** @description Optional Dutch-auction allocation terms */
- auction?: components["schemas"]["DutchAuctionDto"];
- /** @description Unix timestamp deadline; defaults to now + 1800s; must be between now+60s and now+24h */
- deadline?: number;
- /** @description EIP-712 signature for EVM-originated intent creation */
- signature?: string;
- /** @description Unique nonce included in the EIP-712 signature */
- nonce?: string;
- /** @description Unix timestamp when the EIP-712 signature expires */
- expiresAt?: number;
- /** @description Idempotency key for deduplicating duplicate requests */
- idempotencyKey?: string;
+ get?: never;
+ put?: never;
+ /** Exclude an incident intent from griefing ratio */
+ post: operations["SolverGriefingController_excludeIncident"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/tokens": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- DutchAuctionDto: {
- /** @description Starting destination amount in base units */
- startDstAmount: string;
- /** @description Unix timestamp when the price decay starts */
- decayStart: number;
- /** @description Unix timestamp when the price reaches minDstAmount */
- decayEnd: number;
- /** @description Solver address exclusively eligible until exclusivityEnd */
- exclusiveSolver?: string;
- /** @description Unix timestamp when solver exclusivity ends */
- exclusivityEnd?: number;
+ get: operations["TokensController_getTokens"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/tokens/stellar": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- AcceptIntentDto: {
- /** @description Solver address accepting the intent */
- solver: string;
- /** @description Single-use signing nonce */
- nonce?: string;
- /** @description Unix timestamp when the signature expires */
- expiresAt?: number;
- /** @description Base64-encoded Ed25519 signature of the signed intent action */
- signature: string;
+ get: operations["TokensController_getStellarTokens"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/chain/health": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SorobanController_getHealth"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/chain/ledger": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SorobanController_getLatestLedger"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/chain/network": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SorobanController_getNetwork"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/chain/account/{publicKey}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SorobanController_getAccount"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/shadow-report": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Daily shadow-mode divergence summary
+ * @description Returns (expected, simulated) comparison counts for on-chain simulations run in parallel with the off-chain intent path, broken down by transition and by divergence reason, plus a per-UTC-day series and shadow queue health. Headline totals are lifetime-to-date; `days` bounds only the per-day breakdown.
+ */
+ get: operations["ShadowController_shadowReport"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/params": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Current and pending governance-controlled protocol parameters
+ * @description Returns the actively-enforced protocol parameters, any pending governance change with its timelock execution ledger and ETA, and recent parameter history. Parameters are sourced from the on-chain governance contract when PARAMS_CONTRACT_ID is configured; code/env defaults are used otherwise.
+ */
+ get: operations["ParamsController_getParams"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/governance/guardian/status": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Active guardian actions (with tx references) and effective pause state */
+ get: operations["GuardianController_status"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/governance/guardian/actions/{id}/override": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Superadmin override of an active guardian action (audited) */
+ post: operations["GuardianController_override"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/credentials": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List a solver's credentials
+ * @description Lists credential metadata (never plaintext) for the authenticated solver.
+ */
+ get: operations["SolverCredentialController_list"];
+ put?: never;
+ /**
+ * Create a scoped solver credential
+ * @description Mints a new scoped credential for the authenticated solver. The plaintext secret is returned ONCE. Requires a valid SEP-10 JWT for the solver.
+ */
+ post: operations["SolverCredentialController_create"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/credentials/{id}/rotate": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Rotate a solver credential
+ * @description Revokes the credential and issues a replacement with the same scopes. The new plaintext is returned ONCE.
+ */
+ post: operations["SolverCredentialController_rotate"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/credentials/{id}/revoke": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Revoke a solver credential
+ * @description Instantly revokes the credential and propagates to all replicas.
+ */
+ post: operations["SolverCredentialController_revoke"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/jobs/queues": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Depth and dead-letter counts per queue */
+ get: operations["JobsController_queues"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/jobs/queues/{queue}/dead-letters": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Most recent dead-lettered jobs for a queue */
+ get: operations["JobsController_deadLetters"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/flags": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Managed flags with env default, override pin and stored rules */
+ get: operations["FlagsController_list"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/flags/{key}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /**
+ * Set a flag's default and targeting rules
+ * @description Returns 202-style { status: 'pending' } when a second approval is required.
+ */
+ put: operations["FlagsController_update"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/flags/change-requests/{id}/approve": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Approve a pending change (must be a different admin than the proposer) */
+ post: operations["FlagsController_approve"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/flags/{key}/audit": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Audit trail for a flag, newest first */
+ get: operations["FlagsController_auditLog"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/health/live": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["HealthController_live"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/health/ready": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["HealthController_ready"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/health/startup": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["HealthController_startup"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/health": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["HealthController_check"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/stats": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["StatsController_getStats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/stats/treasury": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["StatsController_getTreasuryStats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/stats/ws": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["StatsController_getWsStats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/treasury/reconciliation": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get treasury reconciliation summary
+ * @description Returns daily reconciliation summary showing expected vs actual balances per asset
+ */
+ get: operations["TreasuryController_getReconciliation"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/treasury/reconciliation/{asset}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get detailed reconciliation for an asset
+ * @description Returns detailed reconciliation data including transaction breakdown
+ */
+ get: operations["TreasuryController_getAssetReconciliation"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/treasury/reconciliation/trigger": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Manually trigger treasury reconciliation
+ * @description Admin endpoint to trigger reconciliation on-demand
+ */
+ post: operations["TreasuryController_triggerReconciliation"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+export type webhooks = Record;
+export interface components {
+ schemas: {
+ PauseKillSwitchDto: {
+ /**
+ * @description Breadth of the pause.
+ * @enum {string}
+ */
+ scope: "global" | "chain" | "token" | "operation";
+ /** @description Required for chain/token/operation scope. */
+ chain?: string;
+ /** @description Required for token scope; wildcard for operation scope. */
+ token?: string;
+ /**
+ * @description Required for operation scope.
+ * @enum {string}
+ */
+ operation?: "create" | "accept" | "fill" | "slash" | "onchain";
+ /** @enum {string} */
+ reasonCode: "INCIDENT" | "TOKEN_DEPEGGED" | "SOLVER_INCIDENT" | "CHAIN_DEGRADED" | "RPC_DEGRADED" | "REGULATORY" | "MAINTENANCE";
+ /** @description Operator explanation, shown to clients and in the audit log. */
+ reason: string;
+ };
+ ApproveResumeDto: {
+ /** @description Why the resume is safe. */
+ note?: string;
+ /**
+ * @description Distinct approvals needed.
+ * @default 2
+ */
+ approvalsRequired: number;
+ };
+ CreateIntentDto: {
+ /** @description Stellar address of the user creating the intent */
+ user: string;
+ /**
+ * @description Source chain the funds are coming from
+ * @enum {string}
+ */
+ srcChain: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Source token contract/address on srcChain */
+ srcTokenAddress: string;
+ /** @description Source token symbol, e.g. USDC */
+ srcTokenSymbol: string;
+ /** @description Source token decimals */
+ srcTokenDecimals: number;
+ /** @description Source amount as a non-negative integer string (base units) */
+ srcAmount: string;
+ /** @description Destination Stellar token contract */
+ dstTokenContract: string;
+ /** @description Destination token symbol, e.g. USDC */
+ dstTokenSymbol: string;
+ /** @description Destination token decimals */
+ dstTokenDecimals: number;
+ /** @description Minimum acceptable destination amount as an integer string */
+ minDstAmount: string;
+ /** @description Unix timestamp deadline; defaults to now + 1800s; must be between now+60s and now+24h */
+ deadline?: number;
+ /** @description Idempotency key for deduplicating duplicate requests */
+ idempotencyKey?: string;
+ };
+ BatchLookupDto: {
+ /** @description Intent IDs to look up (max 100). IDs with no matching record are omitted from the response, not individually 404'd. */
+ intentIds: string[];
+ };
+ BatchCreateIntentsDto: {
+ /** @description List of intents to create atomically (1..50) */
+ intents: components["schemas"]["CreateIntentDto"][];
+ };
+ BatchCreateItemErrorDto: {
+ /** @description Zero-based index of the failed intent item in the input array */
+ index: number;
+ /** @description Field name associated with the error, if applicable */
+ field?: string;
+ /** @description Human-readable error description */
+ message: string;
+ };
+ BatchCreateIntentsResponseDto: {
+ /** @description Array of created Intent objects when successful */
+ created: string[];
+ /** @description List of per-item validation errors if any failed */
+ errors: components["schemas"]["BatchCreateItemErrorDto"][];
+ };
+ AcceptIntentDto: {
+ /** @description Solver address accepting the intent */
+ solver: string;
+ /** @description Base64-encoded Ed25519 signature of the message "accept::" produced by the solver's private key */
+ signature: string;
};
FillIntentDto: {
/** @description Solver address filling the intent (must match the accepting solver) */
@@ -517,155 +1193,1117 @@ export interface components {
fillAmount: string;
/** @description Stellar fill transaction hash */
txHash?: string;
- /** @description Single-use signing nonce */
- nonce?: string;
- /** @description Unix timestamp when the signature expires */
- expiresAt?: number;
- /** @description Base64-encoded Ed25519 signature of the signed intent action */
+ /** @description Base64-encoded Ed25519 signature of the message "fill::" produced by the solver's private key */
+ signature: string;
+ };
+ CancelIntentDto: {
+ /** @description Stellar address of the intent's original creator (must match) */
+ user: string;
+ /** @description Base64-encoded Ed25519 signature of the message "cancel:" produced by the private key of `user` */
+ signature: string;
+ };
+ AmendIntentDto: {
+ /** @description Stellar address of the intent's original creator (must match) */
+ user: string;
+ /** @description Replacement minimum destination amount in base units */
+ minDstAmount: string;
+ /** @description Replacement Unix timestamp deadline */
+ deadline: number;
+ /** @description Base64 Ed25519 signature of "amend::::" */
+ signature: string;
+ };
+ QuoteRequestDto: {
+ /**
+ * @description Source chain the funds are coming from
+ * @enum {string}
+ */
+ srcChain: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Source token symbol, e.g. USDC */
+ srcTokenSymbol: string;
+ /** @description Source amount as a non-negative integer string */
+ srcAmount: string;
+ /** @description Destination token symbol, e.g. USDC */
+ dstTokenSymbol: string;
+ /** @description Intent ID to persist the quote to */
+ intentId?: string;
+ /** @description Source token contract address / ID (used for precise token resolution) */
+ srcTokenAddress?: string;
+ /** @description Destination Stellar token contract ID (used for precise token resolution) */
+ dstTokenContract?: string;
+ };
+ RouteStepDto: {
+ /** @enum {string} */
+ type: "bridge" | "swap" | "transfer";
+ /** @description Protocol name, e.g. 'direct-solver', 'uniswap-v3' */
+ protocol: string;
+ fromChain: string;
+ toChain: string;
+ /** @description Source token info for this hop */
+ fromToken: Record;
+ /** @description Destination token info for this hop */
+ toToken: Record;
+ /** @description Estimated execution time in seconds for this step */
+ estimatedTime: number;
+ /** @description Estimated gas cost in the source token's base unit */
+ estimatedGas: string;
+ };
+ RouteDto: {
+ /** @description Ordered list of steps to execute the swap */
+ steps: components["schemas"]["RouteStepDto"][];
+ /** @description Total estimated time for all steps in seconds */
+ totalTime: number;
+ /** @description Total fees in USD across all steps */
+ totalFeesUSD: number;
+ /** @description Estimated price impact as a decimal fraction, e.g. 0.003 = 0.3% */
+ priceImpact: number;
+ };
+ QuoteDto: {
+ /** @description Solver address */
+ solver: string;
+ /** @description Solver name */
+ solverName: string;
+ /** @description Destination amount as a string */
+ dstAmount: string;
+ /** @description Protocol fee as a string */
+ fee: string;
+ /** @description Estimated fill time in seconds */
+ fillTime: number;
+ /** @description Unix timestamp when quote expires */
+ expiresAt: number;
+ /** @description Total fees in USD (protocol fee converted at token price) */
+ totalFeesUSD: number;
+ /** @description Estimated price impact as a decimal fraction, e.g. 0.003 = 0.3% */
+ priceImpact: number;
+ /** @description Computed execution route (direct single-step or multi-hop via USDC intermediate) */
+ route: components["schemas"]["RouteDto"];
+ };
+ QuoteResponseDto: {
+ /** @description Array of quotes sorted by best dstAmount first */
+ quotes: components["schemas"]["QuoteDto"][];
+ /** @description Best quote or null if no solvers available */
+ bestQuote: components["schemas"]["QuoteDto"] | null;
+ /** @description Source chain */
+ srcChain: string;
+ /** @description Source token symbol */
+ srcTokenSymbol: string;
+ /** @description Source amount as a string */
+ srcAmount: string;
+ /** @description Destination token symbol */
+ dstTokenSymbol: string;
+ /** @description Estimated fill time in seconds for the best quote */
+ estimatedFillTime: number;
+ /** @description Total fees in USD for the best quote (0 when no quote available) */
+ totalFeesUSD: number;
+ /** @description Price impact for the best quote as a decimal fraction (0 when no quote available) */
+ priceImpact: number;
+ /** @description True when no solver responded and the returned quote is indicative */
+ indicative?: boolean;
+ };
+ RegisterSolverDto: {
+ /** @description Solver's Stellar address */
+ address: string;
+ /** @description Solver's display name */
+ name: string;
+ /** @description Bond amount as a non-negative integer string (in USDC base units) */
+ bondAmount: string;
+ /** @description Average fill time in seconds */
+ avgFillTime: number;
+ /** @description Chains this solver supports */
+ supportedChains: ("stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche")[];
+ /** @description Token symbols this solver supports */
+ supportedTokens: unknown[][];
+ /** @description Proof-of-control signature for the advertised solver address */
+ proofSignature: string;
+ };
+ UpdateSolverDto: {
+ /** @description New display name */
+ name?: string;
+ /** @description Replacement list of chains this solver supports */
+ supportedChains?: ("stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche")[];
+ /** @description Replacement list of supported token symbols */
+ supportedTokens?: unknown[][];
+ /** @description Updated average fill time in seconds */
+ avgFillTime?: number;
+ /** @description Base64-encoded Ed25519 signature of the message "update-solver:" produced by the solver's private key, proving control of :address */
signature: string;
};
- CancelIntentDto: {
- /** @description Stellar or EVM address of the intent's original creator (must match) */
- user: string;
- /** @description Single-use signing nonce */
- nonce?: string;
- /** @description Unix timestamp when the signature expires */
- expiresAt?: number;
- /** @description Base64 Ed25519 or EIP-712 signature proving control of user */
- signature: string;
+ UpdateSolverStatusDto: {
+ /** @description Proof-of-control signature for the solver status update */
+ signature: string;
+ };
+ StellarTokenDto: {
+ /** @description Stellar contract ID of the token */
+ contract: string;
+ /** @example XLM */
+ symbol: string;
+ /** @example Stellar Lumens */
+ name: string;
+ /** @example 7 */
+ decimals: number;
+ /** @example 0.1182 */
+ priceUSD: number;
+ };
+ StellarTokensResponseDto: {
+ tokens: components["schemas"]["StellarTokenDto"][];
+ };
+ GuardianOverrideDto: Record;
+ CreateSolverCredentialDto: {
+ /**
+ * @description Scopes granted to the credential
+ * @enum {string}
+ */
+ scopes: "solver:read" | "intents:read" | "quote:respond" | "intents:accept" | "intents:fill";
+ /** @description Optional source-address allowlist. Entries are an exact IPv4 address, an IPv4 CIDR block, an exact IPv6 address, or `*` for any source. IPv6 CIDR blocks and hostnames are not supported; an entry that does not parse never matches, so a typo locks the credential out rather than opening it up. */
+ ipAllowlist?: string[];
+ /** @description Unix epoch seconds at which the credential expires (null = never) */
+ expiresAt?: number;
+ };
+ UpdateFlagDto: Record;
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record;
+export interface operations {
+ MetricsController_index: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ KillSwitchController_status: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ KillSwitchController_pause: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["PauseKillSwitchDto"];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ KillSwitchController_approveResume: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ApproveResumeDto"];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_list: {
+ parameters: {
+ query?: {
+ /** @description Filter by intent state */
+ state?: "open" | "accepted" | "filled" | "cancelled" | "expired" | "slashed" | "pending_open" | "pending_accepted" | "pending_filled" | "pending_cancelled";
+ /** @description Filter by user address */
+ user?: string;
+ /** @description Filter by source chain */
+ chain?: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Number of results per page (max 100) */
+ limit?: number;
+ /** @description Cursor for the next page of intents */
+ cursor?: string;
+ /** @description Number of results to skip */
+ offset?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Invalid limit or offset */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_create: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateIntentDto"];
+ };
+ };
+ responses: {
+ /** @description Invalid request body */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Open-intent cap reached — a single user may not hold more than 50 open/accepted intents simultaneously */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Rate limit exceeded — max 10 intent creations per user per 60 s (or 100 req/min per IP globally) */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_listOpen: {
+ parameters: {
+ query?: {
+ /** @description Filter by intent state */
+ state?: "open" | "accepted" | "filled" | "cancelled" | "expired" | "slashed" | "pending_open" | "pending_accepted" | "pending_filled" | "pending_cancelled";
+ /** @description Filter by user address */
+ user?: string;
+ /** @description Filter by source chain */
+ chain?: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Number of results per page (max 100) */
+ limit?: number;
+ /** @description Cursor for the next page of intents */
+ cursor?: string;
+ /** @description Number of results to skip */
+ offset?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_listByUser: {
+ parameters: {
+ query?: {
+ /** @description Filter by intent state */
+ state?: "open" | "accepted" | "filled" | "cancelled" | "expired" | "slashed" | "pending_open" | "pending_accepted" | "pending_filled" | "pending_cancelled";
+ /** @description Filter by user address */
+ user?: string;
+ /** @description Filter by source chain */
+ chain?: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Number of results per page (max 100) */
+ limit?: number;
+ /** @description Cursor for the next page of intents */
+ cursor?: string;
+ /** @description Number of results to skip */
+ offset?: number;
+ };
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_getOne: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_getAudit: {
+ parameters: {
+ query?: {
+ /** @description Filter by intent state */
+ state?: "open" | "accepted" | "filled" | "cancelled" | "expired" | "slashed" | "pending_open" | "pending_accepted" | "pending_filled" | "pending_cancelled";
+ /** @description Filter by user address */
+ user?: string;
+ /** @description Filter by source chain */
+ chain?: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Number of results per page (max 100) */
+ limit?: number;
+ /** @description Cursor for the next page of intents */
+ cursor?: string;
+ /** @description Number of results to skip */
+ offset?: number;
+ };
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Audit trail for the intent */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ intentId?: string;
+ entries?: {
+ /** Format: date-time */
+ timestamp?: string;
+ toState?: string;
+ actor?: string;
+ reason?: string;
+ metadata?: Record | null;
+ }[];
+ };
+ };
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_getPersistedQuote: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Persisted quote for the intent */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent not found or no quote persisted */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_batchLookup: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BatchLookupDto"];
+ };
+ };
+ responses: {
+ /** @description Records for the found intent IDs, plus a count */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description intentIds missing, not an array of strings, or exceeds 100 entries */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_batchCreate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BatchCreateIntentsDto"];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BatchCreateIntentsResponseDto"];
+ };
+ };
+ /** @description Invalid request payload or empty batch */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Per-item validation errors or limits exceeded */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_accept: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AcceptIntentDto"];
+ };
+ };
+ responses: {
+ /** @description Solver not registered or inactive */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent is not in open state */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent has expired */
+ 410: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_fill: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["FillIntentDto"];
+ };
+ };
+ responses: {
+ /** @description Fill amount below minimum */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Wrong solver for this intent */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent is not in accepted state */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Fill window has expired */
+ 410: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description An emergency kill-switch is active for this intent's scope (503 + Retry-After) */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_cancel: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CancelIntentDto"];
+ };
+ };
+ responses: {
+ /** @description Unauthorized */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent is not in open state */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_amend: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AmendIntentDto"];
+ };
+ };
+ responses: {
+ /** @description Unauthorized */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent is not amendable */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_quote: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["QuoteRequestDto"];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["QuoteResponseDto"];
+ };
+ };
+ /** @description Rate limit exceeded — max 20 quote requests per 60 s per IP */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_requote: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["QuoteResponseDto"];
+ };
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent is not in the open state */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Rate limit exceeded — max 20 quote requests per 60 s per IP */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ WsDocsController_getSpec: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_getLegacyLeaderboard: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_register: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["RegisterSolverDto"];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_getLeaderboard: {
+ parameters: {
+ query?: {
+ window?: "24h" | "7d" | "30d" | "all";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_getEligibleIntents: {
+ parameters: {
+ query?: {
+ /** @description Filter by intent state */
+ state?: "open" | "accepted" | "filled" | "cancelled" | "expired" | "slashed" | "pending_open" | "pending_accepted" | "pending_filled" | "pending_cancelled";
+ /** @description Filter by user address */
+ user?: string;
+ /** @description Filter by source chain */
+ chain?: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Number of results per page (max 100) */
+ limit?: number;
+ /** @description Cursor for the next page of intents */
+ cursor?: string;
+ /** @description Number of results to skip */
+ offset?: number;
+ };
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_getSolver: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_updateSolver: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateSolverDto"];
+ };
};
- QuoteRequestDto: {
- /**
- * @description Source chain the funds are coming from
- * @enum {string}
- */
- srcChain: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
- /** @description Source token symbol, e.g. USDC */
- srcTokenSymbol: string;
- /** @description Source amount as a non-negative integer string */
- srcAmount: string;
- /** @description Destination token symbol, e.g. USDC */
- dstTokenSymbol: string;
- /** @description Intent ID to persist the quote to */
- intentId?: string;
- /** @description Source token contract address / ID (used for precise token resolution) */
- srcTokenAddress?: string;
- /** @description Destination Stellar token contract ID (used for precise token resolution) */
- dstTokenContract?: string;
+ responses: {
+ /** @description Updated solver record */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Invalid update body */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Missing or invalid signature */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Solver not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- RouteStepDto: {
- /** @enum {string} */
- type: "bridge" | "swap" | "transfer";
- /** @description Protocol name, e.g. 'direct-solver', 'uniswap-v3' */
- protocol: string;
- fromChain: string;
- toChain: string;
- /** @description Source token info for this hop */
- fromToken: Record;
- /** @description Destination token info for this hop */
- toToken: Record;
- /** @description Estimated execution time in seconds for this step */
- estimatedTime: number;
- /** @description Estimated gas cost in the source token's base unit */
- estimatedGas: string;
+ };
+ SolversController_getSolverStats: {
+ parameters: {
+ query: {
+ window: string;
+ };
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
};
- RouteDto: {
- /** @description Ordered list of steps to execute the swap */
- steps: components["schemas"]["RouteStepDto"][];
- /** @description Total estimated time for all steps in seconds */
- totalTime: number;
- /** @description Total fees in USD across all steps */
- totalFeesUSD: number;
- /** @description Estimated price impact as a decimal fraction, e.g. 0.003 = 0.3% */
- priceImpact: number;
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- QuoteDto: {
- /** @description Solver address */
- solver: string;
- /** @description Solver name */
- solverName: string;
- /** @description Destination amount as a string */
- dstAmount: string;
- /** @description Protocol fee as a string */
- fee: string;
- /** @description Estimated fill time in seconds */
- fillTime: number;
- /** @description Unix timestamp when quote expires */
- expiresAt: number;
- /** @description Total fees in USD (protocol fee converted at token price) */
- totalFeesUSD: number;
- /** @description Estimated price impact as a decimal fraction, e.g. 0.003 = 0.3% */
- priceImpact: number;
- /** @description Computed execution route (direct single-step or multi-hop via USDC intermediate) */
- route: components["schemas"]["RouteDto"];
+ };
+ SolversController_getSlashHistory: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
};
- QuoteResponseDto: {
- /** @description Array of quotes sorted by best dstAmount first */
- quotes: components["schemas"]["QuoteDto"][];
- /** @description Best quote or null if no solvers available */
- bestQuote: components["schemas"]["QuoteDto"] | null;
- /** @description Source chain */
- srcChain: string;
- /** @description Source token symbol */
- srcTokenSymbol: string;
- /** @description Source amount as a string */
- srcAmount: string;
- /** @description Destination token symbol */
- dstTokenSymbol: string;
- /** @description Estimated fill time in seconds for the best quote */
- estimatedFillTime: number;
- /** @description Total fees in USD for the best quote (0 when no quote available) */
- totalFeesUSD: number;
- /** @description Price impact for the best quote as a decimal fraction (0 when no quote available) */
- priceImpact: number;
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- RegisterSolverDto: {
- /** @description Solver's Stellar address */
- address: string;
- /** @description Solver's display name */
- name: string;
- /** @description Bond amount as a non-negative integer string (in USDC base units) */
- bondAmount: string;
- /** @description Average fill time in seconds */
- avgFillTime: number;
- /** @description Chains this solver supports */
- supportedChains: ("stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche")[];
- /** @description Token symbols this solver supports */
- supportedTokens: unknown[][];
- /** @description Proof-of-control signature for the advertised solver address */
- proofSignature: string;
+ };
+ SolversController_submitDispute: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ slashId: string;
+ };
+ cookie?: never;
};
- UpdateSolverDto: {
- /** @description New display name */
- name?: string;
- /** @description Replacement list of chains this solver supports */
- supportedChains?: ("stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche")[];
- /** @description Replacement list of supported token symbols */
- supportedTokens?: unknown[][];
- /** @description Updated average fill time in seconds */
- avgFillTime?: number;
- /** @description Base64-encoded Ed25519 signature of the message "update-solver:" produced by the solver's private key, proving control of :address */
- signature: string;
+ requestBody?: never;
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
};
- responses: never;
- parameters: never;
- requestBodies: never;
- headers: never;
- pathItems: never;
-}
-export type $defs = Record;
-export interface operations {
- HealthController_check: {
+ SolversController_resolveDispute: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ slashId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_deregisterSolver: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateSolverStatusDto"];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_deactivate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateSolverStatusDto"];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_reactivate: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ address: string;
+ };
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateSolverStatusDto"];
+ };
+ };
responses: {
- 200: {
+ 201: {
headers: {
[name: string]: unknown;
};
@@ -673,7 +2311,7 @@ export interface operations {
};
};
};
- SorobanController_getHealth: {
+ SolverGriefingController_listEnforced: {
parameters: {
query?: never;
header?: never;
@@ -682,111 +2320,84 @@ export interface operations {
};
requestBody?: never;
responses: {
- /** @description Soroban RPC node health status (pass-through of the RPC `getHealth` result). */
200: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": {
- /** @example healthy */
- status: string;
- latestLedger?: number;
- oldestLedger?: number;
- ledgerRetentionWindow?: number;
- };
- };
+ content?: never;
};
};
};
- SorobanController_getLatestLedger: {
+ SolverGriefingController_getSolverRecord: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ address: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Latest closed ledger as reported by the Soroban RPC node. */
200: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": {
- id: string;
- /** @example 12345678 */
- sequence: number;
- protocolVersion?: number;
- };
- };
+ content?: never;
};
};
};
- SorobanController_getNetwork: {
+ SolverGriefingController_resetSolver: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ address: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Network passphrase and protocol metadata for the configured RPC node. */
200: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": {
- friendbotUrl?: string | null;
- /** @example Test SDF Network ; September 2015 */
- passphrase: string;
- protocolVersion?: number;
- };
- };
+ content?: never;
};
};
};
- SorobanController_getAccount: {
+ SolverGriefingController_getSolverAudit: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Stellar Ed25519 account public key (starts with `G`, 56 characters). */
- publicKey: string;
+ address: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description On-chain account record (id, sequence number, and balances). */
200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- id: string;
- /** @example 987654321 */
- sequence: string;
- balances?: {
- balance?: string;
- asset_type?: string;
- }[];
- };
- };
- };
- /** @description Invalid Stellar public key format */
- 400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Per-account rate limit exceeded (AccountRateLimitGuard) */
- 429: {
+ };
+ };
+ SolverGriefingController_excludeIncident: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ intentId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 201: {
headers: {
[name: string]: unknown;
};
@@ -877,67 +2488,58 @@ export interface operations {
};
};
};
- IntentsController_list: {
+ SorobanController_getHealth: {
parameters: {
- query: {
- /** @description Filter by intent state */
- state?: string;
- /** @description Filter by user address */
- user?: string;
- /** @description Filter by source chain */
- chain?: string;
- /** @description Number of results per page */
- limit: number;
- /** @description Cursor for the next page of intents */
- cursor?: string;
- /** @description Number of results to skip */
- offset: number;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Invalid limit or offset */
- 400: {
+ /** @description Soroban RPC node health status (pass-through of the RPC `getHealth` result). */
+ 200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": {
+ /** @example healthy */
+ status: string;
+ latestLedger?: number;
+ oldestLedger?: number;
+ ledgerRetentionWindow?: number;
+ };
+ };
};
};
};
- IntentsController_create: {
+ SorobanController_getLatestLedger: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["CreateIntentDto"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Invalid request body */
- 400: {
+ /** @description Latest closed ledger as reported by the Soroban RPC node. */
+ 200: {
headers: {
[name: string]: unknown;
};
- content?: never;
- };
- /** @description Rate limit exceeded — max 10 intent creations per user per 60 s (or 100 req/min per IP globally) */
- 429: {
- headers: {
- [name: string]: unknown;
+ content: {
+ "application/json": {
+ id: string;
+ /** @example 12345678 */
+ sequence: number;
+ protocolVersion?: number;
+ };
};
- content?: never;
};
};
};
- IntentsController_listOpen: {
+ SorobanController_getNetwork: {
parameters: {
query?: never;
header?: never;
@@ -946,46 +2548,60 @@ export interface operations {
};
requestBody?: never;
responses: {
+ /** @description Network passphrase and protocol metadata for the configured RPC node. */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": {
+ friendbotUrl?: string | null;
+ /** @example Test SDF Network ; September 2015 */
+ passphrase: string;
+ protocolVersion?: number;
+ };
+ };
};
};
};
- IntentsController_listByUser: {
+ SorobanController_getAccount: {
parameters: {
query?: never;
header?: never;
path: {
- address: string;
+ /** @description Stellar Ed25519 account public key (starts with `G`, 56 characters). */
+ publicKey: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
+ /** @description On-chain account record (id, sequence number, and balances). */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": {
+ id: string;
+ /** @example 987654321 */
+ sequence: string;
+ balances?: {
+ balance?: string;
+ asset_type?: string;
+ }[];
+ };
+ };
};
- };
- };
- IntentsController_getOne: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- id: string;
+ /** @description Invalid Stellar public key format */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
};
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Intent not found */
- 404: {
+ /** @description Per-account rate limit exceeded (AccountRateLimitGuard) */
+ 429: {
headers: {
[name: string]: unknown;
};
@@ -993,99 +2609,161 @@ export interface operations {
};
};
};
- IntentsController_getAudit: {
+ ShadowController_shadowReport: {
parameters: {
- query?: never;
- header?: never;
- path: {
- id: string;
+ query?: {
+ /** @description Trailing UTC days of per-day breakdown to include (1-90, default 1). */
+ days?: number;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Audit trail for the intent */
+ /** @description Shadow-mode divergence report */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
- intentId?: string;
- entries?: {
- /** Format: date-time */
- timestamp?: string;
- toState?: string;
- actor?: string;
- reason?: string;
- metadata?: Record | null;
+ enabled?: boolean;
+ sampleRate?: number;
+ /** @example 2026-09-30 */
+ day?: string;
+ /** Format: date-time */
+ generatedAt?: string;
+ compared?: number;
+ diverged?: number;
+ divergenceRate?: number;
+ transitions?: {
+ /** @example accept */
+ transition?: string;
+ compared?: number;
+ diverged?: number;
+ divergenceRate?: number;
+ }[];
+ divergences?: {
+ /** @example fill */
+ transition?: string;
+ /**
+ * @example outcome_mismatch
+ * @enum {string}
+ */
+ reason?: "outcome_mismatch" | "simulation_error" | "simulation_exception" | "contract_unconfigured";
+ count?: number;
}[];
+ daily?: {
+ /** @example 2026-09-30 */
+ day?: string;
+ compared?: number;
+ diverged?: number;
+ divergenceRate?: number;
+ cells?: Record[];
+ }[];
+ queue?: {
+ depth?: number;
+ capacity?: number;
+ dropped?: number;
+ sampledOut?: number;
+ disabled?: number;
+ completed?: number;
+ };
};
};
};
- /** @description Intent not found */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
};
};
- IntentsController_getPersistedQuote: {
+ ParamsController_getParams: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Persisted quote for the intent */
+ /** @description Protocol parameters — current, pending, and history */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
- };
- /** @description Intent not found or no quote persisted */
- 404: {
- headers: {
- [name: string]: unknown;
+ content: {
+ "application/json": {
+ /** @description Currently active protocol parameters */
+ current: {
+ /** @example 3 */
+ version: number;
+ /**
+ * @description Protocol fee in basis points
+ * @example 30
+ */
+ feeBps: number;
+ /** @description Per-chain deadline and fill-window overrides */
+ chains: {
+ [key: string]: {
+ /** @example 900 */
+ deadlineSeconds?: number;
+ /** @example 120 */
+ fillWindowSeconds?: number;
+ };
+ };
+ /**
+ * @description Maximum on-chain exposure ratio (0–1)
+ * @example 0.05
+ */
+ maxExposureRatio: number;
+ /** @example 100000000 */
+ slashAmount: string;
+ /** @example 12345678 */
+ activeSinceLedger: number;
+ /** Format: date-time */
+ adoptedAt: string;
+ };
+ /** @description Scheduled governance change not yet activated, or null */
+ pending: ({
+ params?: Record;
+ /** @example 12349999 */
+ executionLedger?: number;
+ /**
+ * Format: date-time
+ * @description Best-effort ETA for timelock execution
+ */
+ estimatedEta?: string;
+ /** Format: date-time */
+ observedAt?: string;
+ } | null) | null;
+ /** @description Previous parameter versions, newest-first (max 50) */
+ history: Record[];
+ };
};
- content?: never;
};
};
};
- IntentsController_getAuctionPrice: {
+ GuardianController_status: {
parameters: {
query?: never;
header?: never;
- path: { id: string };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
- headers: { [name: string]: unknown };
- content: {
- "application/json": {
- intentId: string;
- currentDstAmount: string;
- acceptedDstAmount?: string;
- timestamp: number;
- };
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
};
- 404: { headers: { [name: string]: unknown }; content?: never };
};
};
- IntentsController_accept: {
+ GuardianController_override: {
parameters: {
query?: never;
- header?: never;
+ header: {
+ "x-admin-key": string;
+ };
path: {
id: string;
};
@@ -1093,33 +2771,38 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["AcceptIntentDto"];
+ "application/json": components["schemas"]["GuardianOverrideDto"];
};
};
responses: {
- /** @description Solver not registered or inactive */
- 403: {
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Intent not found */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
+ };
+ };
+ SolverCredentialController_list: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
};
- /** @description Intent is not in open state */
- 409: {
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Credential metadata list */
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Intent has expired */
- 410: {
+ /** @description Missing or invalid SEP-10 JWT */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -1127,51 +2810,62 @@ export interface operations {
};
};
};
- IntentsController_fill: {
+ SolverCredentialController_create: {
parameters: {
query?: never;
header?: never;
path: {
- id: string;
+ address: string;
};
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["FillIntentDto"];
+ "application/json": components["schemas"]["CreateSolverCredentialDto"];
};
};
responses: {
- /** @description Fill amount below minimum */
- 400: {
+ /** @description Credential created (plaintext shown once) */
+ 201: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Wrong solver for this intent */
- 403: {
+ /** @description Missing or invalid SEP-10 JWT */
+ 401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Intent not found */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
+ };
+ };
+ SolverCredentialController_rotate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ id: string;
};
- /** @description Intent is not in accepted state */
- 409: {
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateSolverCredentialDto"];
+ };
+ };
+ responses: {
+ /** @description Credential rotated (new plaintext shown once) */
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Fill window has expired */
- 410: {
+ /** @description Missing or invalid SEP-10 JWT */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -1179,37 +2873,27 @@ export interface operations {
};
};
};
- IntentsController_cancel: {
+ SolverCredentialController_revoke: {
parameters: {
query?: never;
header?: never;
path: {
+ address: string;
id: string;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["CancelIntentDto"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Unauthorized */
- 403: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Intent not found */
- 404: {
+ /** @description Credential revoked */
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Intent is not in open state */
- 409: {
+ /** @description Missing or invalid SEP-10 JWT */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -1217,29 +2901,39 @@ export interface operations {
};
};
};
- IntentsController_quote: {
+ JobsController_queues: {
parameters: {
query?: never;
- header?: never;
+ header: {
+ "x-admin-key": string;
+ };
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["QuoteRequestDto"];
- };
- };
+ requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": components["schemas"]["QuoteResponseDto"];
- };
+ content?: never;
};
- /** @description Rate limit exceeded — max 20 quote requests per 60 s per IP */
- 429: {
+ };
+ };
+ JobsController_deadLetters: {
+ parameters: {
+ query?: never;
+ header: {
+ "x-admin-key": string;
+ };
+ path: {
+ queue: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1247,10 +2941,12 @@ export interface operations {
};
};
};
- SolversController_getLeaderboard: {
+ FlagsController_list: {
parameters: {
query?: never;
- header?: never;
+ header: {
+ "x-admin-key": string;
+ };
path?: never;
cookie?: never;
};
@@ -1264,20 +2960,24 @@ export interface operations {
};
};
};
- SolversController_register: {
+ FlagsController_update: {
parameters: {
query?: never;
- header?: never;
- path?: never;
+ header: {
+ "x-admin-key": string;
+ };
+ path: {
+ key: string;
+ };
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["RegisterSolverDto"];
+ "application/json": components["schemas"]["UpdateFlagDto"];
};
};
responses: {
- 201: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1285,12 +2985,14 @@ export interface operations {
};
};
};
- SolversController_getSolver: {
+ FlagsController_approve: {
parameters: {
query?: never;
- header?: never;
+ header: {
+ "x-admin-key": string;
+ };
path: {
- address: string;
+ id: string;
};
cookie?: never;
};
@@ -1304,44 +3006,71 @@ export interface operations {
};
};
};
- SolversController_updateSolver: {
+ FlagsController_auditLog: {
parameters: {
query?: never;
- header?: never;
+ header: {
+ "x-admin-key": string;
+ };
path: {
- address: string;
+ key: string;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["UpdateSolverDto"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Updated solver record */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Invalid update body */
- 400: {
+ };
+ };
+ HealthController_live: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Missing or invalid signature */
- 401: {
+ };
+ };
+ HealthController_ready: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Solver not found */
- 404: {
+ };
+ };
+ HealthController_startup: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1349,13 +3078,11 @@ export interface operations {
};
};
};
- SolversController_getSolverStats: {
+ HealthController_check: {
parameters: {
query?: never;
header?: never;
- path: {
- address: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
@@ -1368,18 +3095,16 @@ export interface operations {
};
};
};
- SolversController_deregisterSolver: {
+ StatsController_getStats: {
parameters: {
query?: never;
header?: never;
- path: {
- address: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- 201: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1387,18 +3112,16 @@ export interface operations {
};
};
};
- SolversController_deactivate: {
+ StatsController_getTreasuryStats: {
parameters: {
query?: never;
header?: never;
- path: {
- address: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- 201: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1406,18 +3129,16 @@ export interface operations {
};
};
};
- SolversController_reactivate: {
+ StatsController_getWsStats: {
parameters: {
query?: never;
header?: never;
- path: {
- address: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- 201: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1425,37 +3146,143 @@ export interface operations {
};
};
};
- StatsController_getStats: {
+ TreasuryController_getReconciliation: {
parameters: {
- query?: never;
+ query?: {
+ /** @description Date in YYYY-MM-DD format (defaults to today) */
+ date?: string;
+ };
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
+ /** @description Reconciliation summary */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": {
+ /** @example 2026-09-28 */
+ date?: string;
+ assets?: {
+ /** @example native */
+ asset?: string;
+ /** @example 1000000000 */
+ expectedBalance?: string;
+ /** @example 1000500000 */
+ actualBalance?: string;
+ /** @example 500000 */
+ discrepancy?: string;
+ /** @example 0.05 */
+ discrepancyPercentage?: number;
+ /** @example false */
+ hasUnexplainedDiscrepancy?: boolean;
+ explanation?: string | null;
+ }[];
+ /** @example 3 */
+ totalDiscrepancies?: number;
+ /** @example 1 */
+ assetsWithUnexplainedDiscrepancies?: number;
+ /** @example 2026-09-28T00:00:00.000Z */
+ lastReconciliationAt?: string;
+ };
+ };
};
};
};
- StatsController_getWsStats: {
+ TreasuryController_getAssetReconciliation: {
parameters: {
- query?: never;
+ query?: {
+ /** @description Date in YYYY-MM-DD format (defaults to today) */
+ date?: string;
+ };
+ header?: never;
+ path: {
+ /** @description Asset identifier (e.g., 'native', 'USDC:ISSUER', or contract address) */
+ asset: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Detailed reconciliation data */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example 2026-09-28 */
+ snapshotDate?: string;
+ /** @example native */
+ asset?: string;
+ /** @example 1000000000 */
+ expectedBalance?: string;
+ /** @example 1000500000 */
+ actualBalance?: string;
+ /** @example 500000 */
+ discrepancy?: string;
+ /** @example 0.05 */
+ discrepancyPercentage?: number;
+ /** @example 10000000 */
+ toleranceThreshold?: string;
+ /** @example false */
+ hasUnexplainedDiscrepancy?: boolean;
+ explanation?: string | null;
+ breakdown?: {
+ /** @example 500000000 */
+ fees?: string;
+ /** @example 100000000 */
+ slashes?: string;
+ /** @example 50000000 */
+ refunds?: string;
+ };
+ recentTransactions?: {
+ /** @enum {string} */
+ type?: "fee" | "slash" | "refund";
+ /** @example 1000000 */
+ amount?: string;
+ /** @example 2026-09-28T12:00:00.000Z */
+ timestamp?: string;
+ /** @example intent-uuid */
+ reference?: string;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ TreasuryController_triggerReconciliation: {
+ parameters: {
+ query?: {
+ /** @description Specific asset to reconcile (reconciles all if omitted) */
+ asset?: string;
+ };
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
+ /** @description Reconciliation completed */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": {
+ /** @example Reconciliation completed */
+ message?: string;
+ results?: {
+ asset?: string;
+ hasUnexplainedDiscrepancy?: boolean;
+ discrepancy?: string;
+ }[];
+ };
+ };
};
};
};
diff --git a/prisma/migrations/20260927000000_processed_events_dead_letters/down.sql b/prisma/migrations/20260927000000_processed_events_dead_letters/down.sql
new file mode 100644
index 00000000..e16d6816
--- /dev/null
+++ b/prisma/migrations/20260927000000_processed_events_dead_letters/down.sql
@@ -0,0 +1,11 @@
+-- Rollback for 20260927000000_processed_events_dead_letters.
+--
+-- Drops the two event-ing tables this migration creates so a fresh
+-- database returns to its pre-migration (empty) state. The provisional,
+-- network-keyed tables this migration replaced (see the header of
+-- migration.sql) are NOT recreated on rollback: they only exist on
+-- databases that ran 20260926000000 before this migration, and losing
+-- their (superseded) content is the documented limitation of this
+-- rollback. Re-running migration.sql restores the final shape.
+DROP TABLE IF EXISTS "processed_events";
+DROP TABLE IF EXISTS "dead_letter_events";
diff --git a/prisma/migrations/20260927000000_processed_events_dead_letters/migration.sql b/prisma/migrations/20260927000000_processed_events_dead_letters/migration.sql
index f1601055..fa90ef20 100644
--- a/prisma/migrations/20260927000000_processed_events_dead_letters/migration.sql
+++ b/prisma/migrations/20260927000000_processed_events_dead_letters/migration.sql
@@ -1,6 +1,17 @@
-- Migration: processed_events + dead_letter_events
-- These tables support the versioned event decoder registry (#390),
-- ledger-gap backfill (#391), and chain-authoritative reconciler (#392).
+--
+-- 20260926000000_tx_confirmation_channel_pool_ingestion created provisional,
+-- network-keyed tables under these same names as part of its ingestion work.
+-- This migration is the authoritative definition (keyed by the RPC event id
+-- "-"), so on a database that has already run the earlier
+-- migration the provisional tables are dropped and recreated in the final
+-- shape. Without that drop, a fresh `prisma migrate deploy` fails with
+-- "relation already exists".
+
+DROP TABLE IF EXISTS "processed_events";
+DROP TABLE IF EXISTS "dead_letter_events";
-- ─── processed_events ────────────────────────────────────────────────────────
-- Idempotency table: one row per confirmed-processed Soroban event.
@@ -17,12 +28,18 @@ CREATE TABLE "processed_events" (
"processed_at" TIMESTAMPTZ NOT NULL DEFAULT now()
);
+-- squawk-ignore create-index-without-concurrently
+-- justification: Prisma runs each migration inside a transaction, which forbids CREATE INDEX CONCURRENTLY; the table is fresh (or freshly recreated) in this migration, so the build takes no meaningful lock.
CREATE UNIQUE INDEX "processed_events_ledger_idx_key"
ON "processed_events" ("ledger", "event_index");
+-- squawk-ignore create-index-without-concurrently
+-- justification: Prisma runs each migration inside a transaction, which forbids CREATE INDEX CONCURRENTLY; the table is fresh (or freshly recreated) in this migration, so the build takes no meaningful lock.
CREATE INDEX "processed_events_ledger_idx"
ON "processed_events" ("ledger");
+-- squawk-ignore create-index-without-concurrently
+-- justification: Prisma runs each migration inside a transaction, which forbids CREATE INDEX CONCURRENTLY; the table is fresh (or freshly recreated) in this migration, so the build takes no meaningful lock.
CREATE INDEX "processed_events_contract_idx"
ON "processed_events" ("contract_id");
@@ -41,5 +58,7 @@ CREATE TABLE "dead_letter_events" (
"occurred_at" TIMESTAMPTZ NOT NULL DEFAULT now()
);
+-- squawk-ignore create-index-without-concurrently
+-- justification: Prisma runs each migration inside a transaction, which forbids CREATE INDEX CONCURRENTLY; the table is fresh (or freshly recreated) in this migration, so the build takes no meaningful lock.
CREATE INDEX "dead_letter_events_ledger_idx"
ON "dead_letter_events" ("ledger");
diff --git a/prisma/migrations/20261001000000_token_fk_normalization/down.sql b/prisma/migrations/20261001000000_token_fk_normalization/down.sql
new file mode 100644
index 00000000..a2ec7a75
--- /dev/null
+++ b/prisma/migrations/20261001000000_token_fk_normalization/down.sql
@@ -0,0 +1,17 @@
+-- Down: #410 — Normalise intent token data into foreign keys (phase 1).
+--
+-- Reverses the expand/contract phase-1 migration: drops the volume indexes,
+-- the deferred foreign keys, then the FK/snapshot columns. The JSON blobs
+-- (src_token / dst_token) were retained throughout, so no data is lost.
+
+DROP INDEX IF EXISTS "intents_volume_by_dst_token_idx";
+DROP INDEX IF EXISTS "intents_volume_by_token_idx";
+
+ALTER TABLE "intents" DROP CONSTRAINT IF EXISTS "intents_dst_token_id_fkey";
+ALTER TABLE "intents" DROP CONSTRAINT IF EXISTS "intents_src_token_id_fkey";
+
+ALTER TABLE "intents"
+ DROP COLUMN IF EXISTS "dst_decimals",
+ DROP COLUMN IF EXISTS "src_decimals",
+ DROP COLUMN IF EXISTS "dst_token_id",
+ DROP COLUMN IF EXISTS "src_token_id";
diff --git a/prisma/migrations/20261002000000_intent_state_pending_variants/down.sql b/prisma/migrations/20261002000000_intent_state_pending_variants/down.sql
new file mode 100644
index 00000000..6e00c285
--- /dev/null
+++ b/prisma/migrations/20261002000000_intent_state_pending_variants/down.sql
@@ -0,0 +1,12 @@
+-- Rollback for 20261002000000_intent_state_pending_variants.
+--
+-- PostgreSQL (through v16, the version CI and production run) has no
+-- `ALTER TYPE ... DROP VALUE`: enum values cannot be removed once added, and
+-- attempting to recreate the enum would require dropping the `intents` table
+-- and every other column typed with `IntentState`. This rollback is therefore
+-- an explicit no-op — the table list (what the rollback-verification job
+-- compares) is unchanged, and rows in the pending_* states are legal values
+-- of a *superset* enum, so the database remains queryable either way.
+--
+-- Re-running migration.sql re-adds any value that was manually removed.
+SELECT 1;
diff --git a/prisma/migrations/20261002000000_intent_state_pending_variants/migration.sql b/prisma/migrations/20261002000000_intent_state_pending_variants/migration.sql
new file mode 100644
index 00000000..d141c5ea
--- /dev/null
+++ b/prisma/migrations/20261002000000_intent_state_pending_variants/migration.sql
@@ -0,0 +1,15 @@
+-- Issue #385: submitted-but-unconfirmed intent variants.
+--
+-- prisma/schema.prisma's `IntentState` enum declares these four values, but
+-- the init migration only created the original six — a fresh database would
+-- reject `pending_open` / `pending_accepted` / `pending_filled` /
+-- `pending_cancelled` writes at runtime even though the generated client
+-- accepts them. ALTER TYPE ... ADD VALUE (rather than CREATE TYPE) keeps the
+-- statement additive: it never rewrites or locks existing rows.
+--
+-- IF NOT EXISTS keeps the migration re-runnable against databases where a
+-- earlier partial attempt already added a value.
+ALTER TYPE "IntentState" ADD VALUE IF NOT EXISTS 'pending_open';
+ALTER TYPE "IntentState" ADD VALUE IF NOT EXISTS 'pending_accepted';
+ALTER TYPE "IntentState" ADD VALUE IF NOT EXISTS 'pending_filled';
+ALTER TYPE "IntentState" ADD VALUE IF NOT EXISTS 'pending_cancelled';
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index ea66ea02..84341043 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -1,13 +1,3 @@
-// Prisma schema for vortex-backend.
-// This file is the source of truth for `npx prisma generate`; the canonical
-// migration history lives in prisma/migrations/.
-//
-// Issues implemented in this file:
-// #410 — src_token_id / dst_token_id FK columns + snapshot decimals
-// #411 — DATABASE_REPLICA_URLS env var wired through datasource
-
-generator client {
- provider = "prisma-client-js"
// ─── Prisma Schema ────────────────────────────────────────────────────────────
// Database: PostgreSQL (swap to sqlite for local dev / CI without a real DB)
// Run `npm run db:generate` after editing this file.
@@ -44,6 +34,11 @@ enum IntentState {
cancelled
expired
slashed
+ // Submitted-but-unconfirmed variants (issue #385).
+ pending_open
+ pending_accepted
+ pending_filled
+ pending_cancelled
}
enum SupportedChain {
@@ -56,97 +51,16 @@ enum SupportedChain {
avalanche
}
+/// Registry lifecycle. Delisted rows stay in the table so existing intents
+/// that already copied the token metadata keep resolving.
enum TokenStatus {
active
paused
delisted
-}
-
-// ─── Tokens ──────────────────────────────────────────────────────────────────
-model Token {
- id String @id @default(cuid())
- address String
- symbol String
- name String
- decimals Int
- chain SupportedChain
- logoUri String? @map("logo_uri")
- priceUsd Float? @map("price_usd")
- isStellar Boolean @default(false) @map("is_stellar")
- status TokenStatus @default(active)
- assetKind String @default("evm") @map("asset_kind")
-
- // Back-references from intents (#410)
- srcIntents Intent[] @relation("src_token")
- dstIntents Intent[] @relation("dst_token")
-
- @@unique([address, chain], name: "address_chain")
- @@index([chain])
- @@index([symbol])
- @@map("tokens")
+ @@map("token_status")
}
-// ─── Intents ─────────────────────────────────────────────────────────────────
-
-model Intent {
- id String @id @default(cuid())
- intentId String @unique @map("intent_id")
- user String
- srcChain SupportedChain @map("src_chain")
-
- // Original JSON blobs retained during expand/contract migration (#410).
- // Do NOT remove until the follow-up contract migration is complete.
- srcToken Json @map("src_token")
- srcAmount String @map("src_amount")
- dstToken Json @map("dst_token")
- minDstAmount String @map("min_dst_amount")
-
- // ── #410: FK columns ────────────────────────────────────────────────────────
- // Nullable during the transition period; set by the create path once all
- // tokens in the registry are resolvable. The backfill migration populates
- // these for existing rows.
- srcTokenId String? @map("src_token_id")
- dstTokenId String? @map("dst_token_id")
- /// Immutable snapshot of src token decimals at intent creation time.
- srcDecimals Int? @map("src_decimals")
- /// Immutable snapshot of dst token decimals at intent creation time.
- dstDecimals Int? @map("dst_decimals")
-
- // Relations
- srcTokenRecord Token? @relation("src_token", fields: [srcTokenId], references: [id])
- dstTokenRecord Token? @relation("dst_token", fields: [dstTokenId], references: [id])
-
- quotedDstAmount String? @map("quoted_dst_amount")
- acceptedDstAmount String? @map("accepted_dst_amount")
- solver String?
- state IntentState @default(open)
- createdAt Int @map("created_at")
- deadline Int
- filledAt Int? @map("filled_at")
- fillAmount String? @map("fill_amount")
- feeAmount String? @map("fee_amount")
- txHash String? @map("tx_hash")
- slashedAt Int? @map("slashed_at")
- slashReason String? @map("slash_reason")
-
- // Optimistic concurrency (#404)
- version Int @default(0)
- idempotencyKey String? @unique @map("idempotency_key")
-
- // Dutch auction (#429)
- auction Json?
-
- // Source-deposit verification (#403)
- srcVerified Boolean @default(false) @map("src_verified")
- srcTxHash String? @map("src_tx_hash")
- srcVerification Json? @map("src_verification")
-
- // Governance params snapshot at creation
- paramsVersion String? @map("params_version")
-
- // Audit log
- auditLog IntentAuditLog[]
// ─── Kill-switch scopes (issue #477) ──────────────────────────────────────────
// A switch is addressed by exactly one of four mutually-exclusive scopes.
// `global` has no chain/token; `chain` sets chain only; `token` sets chain +
@@ -175,6 +89,15 @@ enum KillSwitchOperation {
onchain
}
+/// Fill-verification lifecycle (issue #385 / DDL in
+/// 20260930000000_fill_verification): `pending` until the source-deposit side
+/// settles, then `verified` or `rejected`.
+enum FillVerificationState {
+ pending
+ verified
+ rejected
+}
+
// ─── Intent ──────────────────────────────────────────────────────────────────
// Represents a cross-chain swap request submitted by a user.
// The nested token objects (srcToken / dstToken) are stored as JSONB so the
@@ -195,6 +118,22 @@ model Intent {
dstToken Json @map("dst_token")
/// Minimum acceptable destination amount (base unit string).
minDstAmount String @map("min_dst_amount")
+
+ // ── #410: FK columns ────────────────────────────────────────────────────────
+ // Nullable during the transition period; set by the create path once all
+ // tokens in the registry are resolvable. The backfill migration populates
+ // these for existing rows.
+ srcTokenId String? @map("src_token_id")
+ dstTokenId String? @map("dst_token_id")
+ /// Immutable snapshot of src token decimals at intent creation time.
+ srcDecimals Int? @map("src_decimals")
+ /// Immutable snapshot of dst token decimals at intent creation time.
+ dstDecimals Int? @map("dst_decimals")
+
+ /// Optional off-chain Dutch auction terms (issue #507).
+ auction Json? @map("auction")
+ /// Auction price locked when a solver accepts.
+ acceptedDstAmount String? @map("accepted_dst_amount")
/// Best quote from solvers, populated after quoting.
quotedDstAmount String? @map("quoted_dst_amount")
/// Solver address that accepted / filled this intent.
@@ -212,38 +151,41 @@ model Intent {
feeAmount String? @map("fee_amount")
/// On-chain transaction hash of the Stellar fill transaction.
txHash String? @map("tx_hash")
+ /// Unix epoch seconds – set when state becomes `slashed`.
+ slashedAt Int? @map("slashed_at")
+ slashReason String? @map("slash_reason")
+ /// Optimistic-concurrency version, incremented on every update (issue #405).
+ version Int @default(0) @map("version")
+ /// Caller-supplied idempotency key for POST /intents; unique across replicas (issue #404).
+ idempotencyKey String? @unique @map("idempotency_key")
+ /// Source-chain escrow deposit confirmed (issue #403); unverified intents are not fillable.
+ srcVerified Boolean @default(false) @map("src_verified")
+ /// EVM deposit transaction hash supplied at creation, if any.
+ srcTxHash String? @map("src_tx_hash")
+ /// Last verification result: status, block, received amount, detail.
+ srcVerification Json? @map("src_verification")
+
+ /// USD value of `srcAmount` at creation (issue #440); null when the price
+ /// was unknown — historical rows are never backfilled.
+ usdValueAtCreate Float? @map("usd_value_at_create")
+ /// Fill-verification timestamp written by the fill verifier.
+ fillVerifiedAt DateTime? @map("fill_verified_at") @db.Timestamptz
+ /// Fill verification state ("pending" | "verified" | "rejected").
+ fillVerificationState FillVerificationState? @map("fill_verification_state")
+ /// Reason reported when fill verification rejected the fill.
+ fillVerificationReason String? @map("fill_verification_reason")
@@index([user])
@@index([state])
- @@index([solver])
@@index([user, createdAt(sort: Desc)], name: "intents_user_created_idx")
@@index([state, createdAt(sort: Desc)], name: "intents_state_created_idx")
- // Volume-by-token index for token analytics (#410)
- // Partial indexes are expressed in raw SQL migration (Prisma doesn't support WHERE clauses).
+ // Volume-by-token index for token analytics (#410).
+ // Partial variants live in raw SQL migration 20261001000000 (Prisma cannot
+ // express WHERE clauses on indexes).
@@index([srcTokenId, createdAt(sort: Desc)], name: "intents_volume_by_token_idx")
@@index([dstTokenId, createdAt(sort: Desc)], name: "intents_volume_by_dst_token_idx")
@@map("intents")
-}
-// ─── Solvers ─────────────────────────────────────────────────────────────────
-
-model Solver {
- id String @id @default(cuid())
- address String @unique
- name String
- bondAmount String @map("bond_amount")
- fillsCompleted Int @default(0) @map("fills_completed")
- fillsFailed Int @default(0) @map("fills_failed")
- totalVolume String @default("0") @map("total_volume")
- avgFillTime Float @default(0) @map("avg_fill_time")
- isActive Boolean @default(true) @map("is_active")
- registeredAt Int @map("registered_at")
- lastActiveAt Int @map("last_active_at")
- supportedChains Json @map("supported_chains")
- supportedTokens Json @map("supported_tokens")
- source String? @default("api")
- chainUpdatedLedger Int? @map("chain_updated_ledger")
- @@map("intents")
}
// ─── Solver ──────────────────────────────────────────────────────────────────
@@ -284,23 +226,6 @@ model Solver {
@@map("solvers")
}
-// ─── Intent audit log ────────────────────────────────────────────────────────
-
-model IntentAuditLog {
- id BigInt @id @default(autoincrement())
- intentId String @map("intent_id")
- timestamp DateTime @default(now())
- toState String @map("to_state")
- actor String
- reason String
- metadata Json?
-
- intent Intent @relation(fields: [intentId], references: [intentId], onDelete: Cascade)
-
- @@index([intentId])
- @@index([intentId, timestamp(sort: Desc)])
- @@map("intent_audit_log")
-}
// ─── IntentAuditLog ──────────────────────────────────────────────────────────
// Append-only record of every state transition for an intent (issue #217 / #62).
// Queried by intentId to reconstruct the full history of a swap.
@@ -324,6 +249,30 @@ model IntentAuditLog {
@@map("intent_audit_log")
}
+// ─── ContractUpgrade ─────────────────────────────────────────────────────────
+// Append-only history of detected WASM upgrades of the contracts the backend
+// writes to (issue #402). Written by ContractVersionService when a poll sees
+// a new hash or an upgrade event is ingested.
+
+model ContractUpgrade {
+ id BigInt @id @default(autoincrement()) @map("id")
+ /// "settlement" | "solverRegistry"
+ contractName String @map("contract_name")
+ contractId String @map("contract_id")
+ previousWasmHash String? @map("previous_wasm_hash")
+ wasmHash String? @map("wasm_hash")
+ /// ABI version the new hash maps to; null when unsupported.
+ abiVersion String? @map("abi_version")
+ /// "poll" | "event"
+ source String @map("source")
+ ledger Int? @map("ledger")
+ txHash String? @map("tx_hash")
+ detectedAt DateTime @default(now()) @map("detected_at") @db.Timestamptz
+
+ @@index([contractId, detectedAt], name: "contract_upgrades_contract_idx")
+ @@map("contract_upgrades")
+}
+
// ─── Token ───────────────────────────────────────────────────────────────────
// Static registry of tokens the protocol supports.
// Kept separate so it can be updated without migrations when the token list changes.
@@ -341,6 +290,12 @@ model Token {
priceUsd Float? @map("price_usd")
/// Whether this is a destination-side Stellar token.
isStellar Boolean @default(false) @map("is_stellar")
+ /// active tokens are listed; paused stay listed but are not offered for new
+ /// intents; delisted are hidden from discovery and still readable by id.
+ status TokenStatus @default(active) @map("status")
+ /// evm | stellar-sac | stellar-classic. String so classic and SAC stay distinct
+ /// without a second database enum.
+ assetKind String @default("evm") @map("asset_kind")
@@unique([address, chain])
@@index([chain])
@@ -610,3 +565,278 @@ model GuardianAction {
@@map("guardian_actions")
}
+// ─── OnchainOutbox ───────────────────────────────────────────────────────────
+// Transactional outbox for on-chain writes (issue #396). A row is inserted in
+// the same database transaction as the intent mutation it mirrors, and
+// OutboxRelayService submits it to Soroban afterwards — so the DB can never say
+// "accepted" while the corresponding transaction silently never went out.
+//
+// `id` is a monotonically increasing sequence and doubles as the per-intent
+// ordering key: a row is only claimable once every earlier row for the same
+// intent has reached a terminal success state.
+
+enum OutboxStatus {
+ pending
+ processing
+ submitted
+ confirmed
+ simulated
+ dead
+}
+
+model OnchainOutbox {
+ id BigInt @id @default(autoincrement()) @map("id")
+ /// intents.intent_id this operation belongs to (ordering partition key).
+ intentId String @map("intent_id")
+ /// Contract operation, e.g. "create_intent", "accept_intent".
+ operation String @map("operation")
+ /// Operation arguments (JSON-safe, bigint amounts as strings).
+ payload Json @map("payload")
+ status OutboxStatus @default(pending) @map("status")
+ /// Number of times this row has been claimed by a relay worker.
+ attempts Int @default(0) @map("attempts")
+ nextAttemptAt DateTime @default(now()) @map("next_attempt_at") @db.Timestamptz
+ /// Lease expiry while status = processing; a crashed worker's row is reclaimed after this.
+ lockedUntil DateTime? @map("locked_until") @db.Timestamptz
+ /// Hash of the signed envelope, persisted BEFORE submission (crash idempotency).
+ envelopeHash String? @map("envelope_hash")
+ /// Hash of the transaction that was submitted.
+ txHash String? @map("tx_hash")
+ lastError String? @map("last_error")
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz
+
+ @@index([status, nextAttemptAt], name: "onchain_outbox_claim_idx")
+ @@index([intentId, id], name: "onchain_outbox_intent_order_idx")
+ @@map("onchain_outbox")
+}
+
+// ─── PendingSlash ────────────────────────────────────────────────────────────
+// Durable saga state for a solver slash (issue #397):
+// detected → challenge_window → submitted → confirmed | cancelled
+// The unique constraint on intent_id enforces exactly-once slashing per intent.
+
+enum PendingSlashState {
+ detected
+ challenge_window
+ submitted
+ confirmed
+ cancelled
+}
+
+model PendingSlash {
+ id String @id @default(uuid()) @map("id")
+ intentId String @unique @map("intent_id")
+ solverAddress String @map("solver_address")
+ reason String @map("reason")
+ state PendingSlashState @default(detected) @map("state")
+ /// The intent's fill deadline (unix seconds) that the solver missed.
+ fillDeadline Int @map("fill_deadline")
+ detectedAt DateTime @map("detected_at") @db.Timestamptz
+ /// Slash may not be submitted before this instant.
+ challengeEndsAt DateTime @map("challenge_ends_at") @db.Timestamptz
+ attempts Int @default(0) @map("attempts")
+ nextAttemptAt DateTime @default(now()) @map("next_attempt_at") @db.Timestamptz
+ /// Lease held by the pipeline worker that is currently verifying/submitting.
+ lockedUntil DateTime? @map("locked_until") @db.Timestamptz
+ txHash String? @map("tx_hash")
+ /// true when the registry client simulated but did not broadcast (dry-run / gated submit).
+ simulated Boolean @default(false) @map("simulated")
+ submittedAt DateTime? @map("submitted_at") @db.Timestamptz
+ confirmedAt DateTime? @map("confirmed_at") @db.Timestamptz
+ cancelledAt DateTime? @map("cancelled_at") @db.Timestamptz
+ cancelReason String? @map("cancel_reason")
+ cancelledBy String? @map("cancelled_by")
+ /// Fill transaction that cancelled the slash, when cancelled by a fill proof.
+ fillTxHash String? @map("fill_tx_hash")
+ lastError String? @map("last_error")
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz
+
+ @@index([state, challengeEndsAt], name: "pending_slashes_due_idx")
+ @@index([solverAddress], name: "pending_slashes_solver_idx")
+ @@map("pending_slashes")
+}
+
+// ─── API key tiers (issue #441) ───────────────────────────────────────────────
+// API keys authenticate programmatic clients and carry a tier that selects the
+// distributed rate-limit quota. Keys are stored only as SHA-256 hashes; the
+// plaintext is shown once at creation and never persisted or logged.
+
+enum ApiKeyTier {
+ public
+ integrator
+ solver
+ partner
+}
+
+model ApiKey {
+ id String @id @default(uuid()) @map("id")
+ /// First 8 characters of the key — non-secret lookup handle.
+ keyPrefix String @unique @map("key_prefix")
+ /// SHA-256 hash of the full key (hex-encoded). Never store the plaintext.
+ keyHash String @map("key_hash")
+ tier ApiKeyTier @map("tier")
+ /// Free-text owner label (e.g. "frontend", "solver-bot-1").
+ owner String @map("owner")
+ /// Optional scope strings narrowing what the key may do (e.g. ["intents:read"]).
+ scopes Json @default("[]") @map("scopes")
+ /// Unix epoch seconds.
+ createdAt Int @map("created_at")
+ /// Unix epoch seconds; null while the key is active.
+ revokedAt Int? @map("revoked_at")
+ /// Unix epoch seconds; null for keys that do not expire.
+ expiresAt Int? @map("expires_at")
+ lastUsedAt Int? @map("last_used_at")
+
+ @@index([tier])
+ @@map("api_keys")
+}
+
+model SolverCredential {
+ id String @id @default(uuid()) @map("id")
+ /// First 8 characters of the credential — non-secret lookup handle.
+ credPrefix String @unique @map("cred_prefix")
+ /// SHA-256 hash of the credential (hex-encoded).
+ credHash String @map("cred_hash")
+ /// Solver on-chain address this credential is bound to.
+ solverAddress String @map("solver_address")
+ /// Allowed scope strings, e.g. ["quote:respond", "intents:accept"].
+ scopes Json @map("scopes")
+ /// Optional CIDR / IP allowlist the credential may be presented from.
+ ipAllowlist Json? @map("ip_allowlist")
+ /// Unix epoch seconds.
+ createdAt Int @map("created_at")
+ /// Unix epoch seconds; null for credentials that do not expire.
+ expiresAt Int? @map("expires_at")
+ /// Unix epoch seconds; null while the credential is active.
+ revokedAt Int? @map("revoked_at")
+ /// Unix epoch seconds of the last rotation (previous credential revoked).
+ rotatedAt Int? @map("rotated_at")
+ lastUsedAt Int? @map("last_used_at")
+
+ @@index([solverAddress])
+ @@map("solver_credentials")
+}
+
+// ─── Soroban event ingestion (transactional outbox lineage) ───────────────────
+// Idempotency + dead-letter tables for the versioned decoder registry (#390),
+// ledger-gap backfill (#391) and chain-authoritative reconciler (#392). The
+// models mirror the authoritative DDL in
+// 20260927000000_processed_events_dead_letters (keyed by the RPC event id
+// "-").
+
+model ProcessedEvent {
+ id BigInt @id @default(autoincrement()) @map("id")
+ /// RPC event id "-".
+ eventId String @map("event_id")
+ ledger Int @map("ledger")
+ eventIndex Int @map("event_index")
+ contractId String @map("contract_id")
+ /// Decoded topic[0] string.
+ topic String @map("topic")
+ txHash String @map("tx_hash")
+ processedAt DateTime @default(now()) @map("processed_at") @db.Timestamptz
+
+ @@unique([ledger, eventIndex], name: "processed_events_ledger_idx_key")
+ @@index([ledger], name: "processed_events_ledger_idx")
+ @@index([contractId], name: "processed_events_contract_idx")
+ @@map("processed_events")
+}
+
+model DeadLetterEvent {
+ id BigInt @id @default(autoincrement()) @map("id")
+ /// RPC event id "-".
+ eventId String @map("event_id")
+ ledger Int @map("ledger")
+ txHash String @map("tx_hash")
+ /// Raw (undecoded) topic[0] string that failed schema validation.
+ rawTopic String @map("raw_topic")
+ /// Validation error message.
+ error String @map("error")
+ /// JSON-encoded base64 XDR topic array, kept so operators can replay.
+ rawXdr String @map("raw_xdr")
+ occurredAt DateTime @default(now()) @map("occurred_at") @db.Timestamptz
+
+ @@index([ledger], name: "dead_letter_events_ledger_idx")
+ @@map("dead_letter_events")
+}
+
+// ─── Pending-transaction confirmation (issues #386–#389, PR #509) ────────────
+// DDL: 20260926000000_tx_confirmation_channel_pool_ingestion.
+
+enum PendingTxStatus {
+ pending
+ confirmed
+ failed
+ expired
+}
+
+model PendingTransaction {
+ id BigInt @id @default(autoincrement()) @map("id")
+ /// The Stellar transaction hash.
+ txHash String @unique @map("tx_hash")
+ /// XDR-encoded signed transaction envelope (base64), kept for fee-bump resubmission.
+ txXdr String @map("tx_xdr")
+ /// Associated intent identifier (nullable — not every tx is tied to one intent).
+ intentId String? @map("intent_id")
+ /// Channel account public key used for submission (nullable for main-key txs).
+ channelKey String? @map("channel_key")
+ status PendingTxStatus @default(pending) @map("status")
+ /// Unix epoch seconds after which tracking is abandoned and the tx is marked expired.
+ maxTrackUntil Int @map("max_track_until")
+ /// Number of poll attempts so far.
+ attempts Int @default(0) @map("attempts")
+ /// Unix epoch seconds of the next scheduled poll.
+ nextPollAt Int @default(0) @map("next_poll_at")
+ /// Fee in stroops of the most recent submission.
+ lastFeeStroops String? @map("last_fee_stroops")
+ /// Number of fee-bump escalations applied so far.
+ feeBumpCount Int @default(0) @map("fee_bump_count")
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
+
+ @@index([status, nextPollAt], name: "pending_tx_poll_idx")
+ @@index([intentId], name: "pending_tx_intent_idx")
+ @@map("pending_transactions")
+}
+
+model IngestionCursor {
+ id BigInt @id @default(autoincrement()) @map("id")
+ network String @map("network")
+ contractId String @map("contract_id")
+ /// Last ledger sequence whose events have been fully applied.
+ lastLedger Int @map("last_ledger")
+ /// Index of the last applied event within lastLedger (for sub-ledger resume).
+ lastEventIdx Int @default(0) @map("last_event_idx")
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
+
+ @@unique([network, contractId], name: "ingestion_cursor_uniq")
+ @@map("ingestion_cursor")
+}
+
+model SignatureNonce {
+ signer String @map("signer")
+ nonce String @map("nonce")
+ expiresAt Int @map("expires_at")
+
+ @@id([signer, nonce])
+ @@index([expiresAt])
+ @@map("signature_nonces")
+}
+
+// ─── Price history (price-feed worker, issue #566) ────────────────────────────
+// Append-only series backing the extreme-move circuit breaker; `tokens.price_usd`
+// holds the latest value, this table keeps the trail.
+
+model PriceHistory {
+ id String @id @default(uuid()) @map("id")
+ /// Token this sample belongs to (cascade-deleted with the token).
+ tokenId String @map("token_id")
+ priceUsd Float @map("price_usd")
+ /// Unix epoch seconds… wall-clock sample time (UTC).
+ recordedAt DateTime @map("recorded_at") @db.Timestamptz
+
+ @@index([tokenId, recordedAt], name: "price_history_token_idx")
+ @@map("price_history")
+}
diff --git a/scripts/README.md b/scripts/README.md
index 2f3a9554..629e87d9 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -45,6 +45,113 @@ npm run solver:demo # in another, run the bot
| `SOLVER_CHAINS` | `stellar,ethereum,base,polygon,arbitrum,optimism,avalanche` | Comma-separated list of chain topics to filter WebSocket intent events |
| `MIN_MARGIN_BPS` | `0` | Minimum margin threshold in basis points for filtering unviable intent fills |
+The bot's accept gate (state → deadline → chain → margin) is shared with
+the simulation harness below — see `tools/simulator/strategies/gate.ts`.
+
+---
+
+# tools/simulator
+
+Solver simulation & backtesting harness (issue #452). Replays an archived
+intent stream against pluggable solver strategies on a **deterministic
+simulated clock** — no network, no Nest bootstrap, no wall-clock reads —
+and reports **PnL, fill-rate and slash-risk**, optionally across a
+fill-window × fee-bps parameter sweep.
+
+The harness reuses the pure domain modules directly:
+
+- `src/fees` — `quoteFee` prices every fill under the configured fee bps
+- `src/routing` — `RoutingService.buildRoute` gates settlement feasibility
+- `src/auctions` — `dutchAuctionPrice` drives auction-aware fills
+
+`npm run typecheck`/`npm run lint` cover it the usual way; the harness's
+suite runs in the unit test project `tools` (`tools/simulator/*.spec.ts`).
+
+## Usage
+
+```bash
+# Replay an archive with the default margin-aware strategy
+npm run simulate -- --input tools/simulator/fixtures/sample-intents.jsonl \
+ --prices tools/simulator/fixtures/sample-prices.jsonl
+
+# Baseline: the naive accept-everything strategy (same gate as scripts/solver-bot.ts)
+npm run simulate -- --input archive.jsonl --prices prices.jsonl --strategy always
+
+# Comparative sweep: fill window × fee bps → markdown table
+npm run simulate -- --input archive.jsonl --prices prices.jsonl --sweep \
+ --fill-windows 60,120,300 --fee-bps-list 0,5,10
+
+# Smoke-run without data (deterministic synthetic archive) + raw report dump
+npm run simulate -- --generate 100000 --strategy always --json report.json
+
+# Full option list
+npm run simulate -- --help
+```
+
+## Data format
+
+Archived intents are **JSON Lines**, one event per line. Field names
+mirror the public `intents` dataset schema (`src/datasets/schemas.ts`), so
+exported dataset rows replay with minimal reshaping:
+
+| Field | Type | Required | Notes |
+|---|---|---|---|
+| `intentId` | string | ✔ | |
+| `createdAt`, `deadline` | int (unix s) | ✔ | `createdAt` positions the simulated clock |
+| `srcChain`, `srcAmount`, `minDstAmount` | string | ✔ | amounts in base units |
+| `srcTokenSymbol`, `dstTokenSymbol` | string | | enables USD valuation |
+| `usdValueAtCreate` | number | | source-leg value captured at creation; preferred over price lookups |
+| `auction` | object | | `{ startDstAmount, decayStart, decayEnd }` Dutch auction |
+| `event` | `"intent"` \| `"quote_request"` | | dispatch selector (default `intent`) |
+
+Prices are JSONL snapshots `{"ts","chain","symbol","priceUsd","decimals?"}`;
+lookups resolve to the latest snapshot at or before the simulated time.
+**Unknown prices are never guessed** — fills with unpriceable legs are
+counted but excluded from USD totals (reported as `unpriced fills`).
+
+Blank lines and `#` comments are ignored in both formats.
+
+## Strategies & the strategy interface
+
+Implement `SimulatorStrategy` (`tools/simulator/types.ts`):
+
+```ts
+interface SimulatorStrategy {
+ readonly name: string;
+ onIntent(ctx, intent): QuoteDecision | null; // new intent on the feed
+ onQuoteRequest(ctx, intent): QuoteDecision | null; // RFQ round
+ onTick(ctx, nowSec): void; // clock advanced
+}
+```
+
+Returning `null` declines; returning `{ dstAmount, fillDelayMs? }`
+schedules a fill at `now + fillDelayMs`, which the engine checks against
+the intent deadline and the fill-window parameter. `ctx.random()` is a
+seeded PRNG, so even randomized strategies replay identically per seed.
+
+Two reference strategies ship with the harness:
+
+| Strategy | Behaviour |
+|---|---|
+| `always` (`AlwaysFillStrategy`) | quotes `minDstAmount` immediately for every gate-passing intent — the exact strategy `scripts/solver-bot.ts` runs live, extracted for offline replay |
+| `margin` (`MarginThresholdStrategy`, default) | values both legs via archived prices, quotes the protocol fee, checks routing feasibility, and optionally waits for Dutch-auction decay (`--auction`) before filling |
+
+## Sweep mode
+
+`--sweep` replays the same archive once per grid cell
+(`--fill-windows` × `--fee-bps-list`, defaults `60,120,300` × `0,5,10`)
+and prints one comparative markdown row per cell: fill rate, capture,
+slashes, PnL, fees, volume. `--json` dumps the full structured sweep.
+
+## Determinism & performance
+
+The engine consumes only archive timestamps (never `Date.now()`), and all
+randomness flows through the seeded PRNG — so identical
+(archive, prices, strategy, params, seed) inputs yield byte-identical
+reports. This is asserted by the determinism tests in
+`tools/simulator/engine.spec.ts`, and `tools/simulator/perf.spec.ts`
+enforces the issue's budget: **1M intents replay in under 5 minutes**.
+
---
# scripts/backfill-events.ts
diff --git a/scripts/solver-bot.ts b/scripts/solver-bot.ts
index 88444899..c4363ea4 100644
--- a/scripts/solver-bot.ts
+++ b/scripts/solver-bot.ts
@@ -2,6 +2,7 @@
import WebSocket from "ws";
import { Keypair } from "@stellar/stellar-sdk";
import { buildAcceptMessage, buildFillMessage } from "../src/common/stellar-signature";
+import { attemptGateReason } from "../tools/simulator/strategies/gate";
const API_BASE = process.env.API_BASE ?? "http://localhost:4000";
const WS_URL = process.env.WS_URL ?? "ws://localhost:4000/ws";
@@ -104,13 +105,20 @@ async function fillIntent(intentId: string, minDstAmount: string): Promise
async function tryFillOpenIntent(intent: Intent): Promise {
const now = Math.floor(Date.now() / 1000);
- if (intent.state !== "open" || intent.deadline <= now) return;
- if (!SOLVER_CHAINS.includes(intent.srcChain)) {
+ // Shared accept gate — the same one tools/simulator's always-fill
+ // reference strategy runs in replay (issue #452). Live bot and harness
+ // agree on which intents get attempted.
+ const reason = attemptGateReason(intent, {
+ chains: SOLVER_CHAINS,
+ minMarginBps: MIN_MARGIN_BPS,
+ nowSec: now,
+ });
+ if (reason === "state" || reason === "deadline") return;
+ if (reason === "chain") {
console.log(`[solver-bot] skipping ${intent.intentId} on ${intent.srcChain} (not subscribed)`);
return;
}
-
- if (MIN_MARGIN_BPS > 0 && Number(intent.minDstAmount) < 1_000_000 * (MIN_MARGIN_BPS / 10000)) {
+ if (reason === "margin") {
console.log(`[solver-bot] skipped ${intent.intentId} below min margin ${MIN_MARGIN_BPS} bps`);
return;
}
diff --git a/src/abuse/abuse-detector.guard.ts b/src/abuse/abuse-detector.guard.ts
index d3188d64..bbf3e023 100644
--- a/src/abuse/abuse-detector.guard.ts
+++ b/src/abuse/abuse-detector.guard.ts
@@ -27,7 +27,6 @@ import {
HttpException,
Injectable,
Logger,
- TooManyRequestsException,
} from "@nestjs/common";
import { Request } from "express";
import { AbuseScoreService } from "./abuse-score.service";
@@ -45,6 +44,19 @@ class PreconditionRequiredException extends HttpException {
}
}
+/**
+ * 429 Too Many Requests for the throttle action.
+ *
+ * Declared locally because the installed `@nestjs/common` build does not
+ * export a `TooManyRequestsException`; mirroring {@link PreconditionRequiredException}
+ * keeps the guard independent of that export.
+ */
+class TooManyRequestsException extends HttpException {
+ constructor(message: string) {
+ super({ statusCode: 429, error: "Too Many Requests", message }, 429);
+ }
+}
+
@Injectable()
export class AbuseDetectorGuard implements CanActivate {
private readonly logger = new Logger(AbuseDetectorGuard.name);
diff --git a/src/abuse/abuse-score.service.spec.ts b/src/abuse/abuse-score.service.spec.ts
index 778afb00..b51a1993 100644
--- a/src/abuse/abuse-score.service.spec.ts
+++ b/src/abuse/abuse-score.service.spec.ts
@@ -13,6 +13,7 @@
import { ConfigService } from "@nestjs/config";
import { AbuseScoreService } from "./abuse-score.service";
import { AbuseContext } from "./abuse.types";
+import type { AppConfig } from "../config/configuration";
// ---------------------------------------------------------------------------
// Minimal ioredis mock — we stub only the methods used by the scorer.
@@ -133,10 +134,10 @@ class FakeRedis {
// Helpers
// ---------------------------------------------------------------------------
-function makeConfigService(): ConfigService {
+function makeConfigService(): ConfigService {
return {
get: (_key: string) => "redis://localhost:6379",
- } as unknown as ConfigService;
+ } as unknown as ConfigService;
}
function makeCtx(overrides: Partial = {}): AbuseContext {
@@ -153,7 +154,7 @@ function makeCtx(overrides: Partial = {}): AbuseContext {
}
function buildService(fakeRedis: FakeRedis): AbuseScoreService {
- const svc = new AbuseScoreService(makeConfigService() as ConfigService);
+ const svc = new AbuseScoreService(makeConfigService());
// Replace the ioredis instance with our fake
(svc as unknown as { redis: FakeRedis }).redis = fakeRedis;
return svc;
diff --git a/src/abuse/allowlist.service.ts b/src/abuse/allowlist.service.ts
index 1a37d1d7..728c15d8 100644
--- a/src/abuse/allowlist.service.ts
+++ b/src/abuse/allowlist.service.ts
@@ -39,8 +39,9 @@ export class AllowlistService {
for (const entry of raw.split(",").map((e) => e.trim()).filter(Boolean)) {
if (entry.startsWith("key:")) {
keyDigests.add(this.digest(entry.slice(4)));
- } else if (entry.startsWith("G") && entry.length >= 32) {
- // Stellar address heuristic: starts with G and is at least 32 chars
+ } else if (entry.startsWith("G")) {
+ // Stellar account IDs start with G (contracts C, muxed M never do),
+ // so a G-prefix alone classifies the entry as an address.
addresses.add(entry.toLowerCase());
} else {
// Everything else is treated as an IP / CIDR
diff --git a/src/archival/archival.service.ts b/src/archival/archival.service.ts
index 9c7d0736..ca764320 100644
--- a/src/archival/archival.service.ts
+++ b/src/archival/archival.service.ts
@@ -106,7 +106,7 @@ export class ArchivalService {
let cursor: string | undefined;
let fileIndex = 0;
- while (true) {
+ for (;;) {
const batch = await this.prisma.withStatsTimeout((tx) =>
(tx as unknown as typeof this.prisma).intent.findMany({
where: {
@@ -204,7 +204,7 @@ export class ArchivalService {
let auditCursor = 0n;
let auditFileIndex = 0;
- while (true) {
+ for (;;) {
const auditBatch = await this.prisma.withStatsTimeout((tx) =>
(tx as unknown as typeof this.prisma).intentAuditLog.findMany({
where: {
diff --git a/src/archival/parquet-writer.ts b/src/archival/parquet-writer.ts
index 2e06b2e6..d7062774 100644
--- a/src/archival/parquet-writer.ts
+++ b/src/archival/parquet-writer.ts
@@ -3,7 +3,7 @@ import { join } from "node:path";
import { readFileSync, unlinkSync } from "node:fs";
import { randomBytes } from "node:crypto";
// @dsnp/parquetjs is aliased as "parquetjs" in package.json
-// eslint-disable-next-line @typescript-eslint/no-require-imports
+// eslint-disable-next-line @typescript-eslint/no-var-requires
const parquet = require("parquetjs");
/**
diff --git a/src/common/http-egress/http-egress.service.ts b/src/common/http-egress/http-egress.service.ts
index 8cffc8a4..345c60e7 100644
--- a/src/common/http-egress/http-egress.service.ts
+++ b/src/common/http-egress/http-egress.service.ts
@@ -249,7 +249,8 @@ export class HttpEgressService {
if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
redirects++;
- const location = response.headers['location'];
+ const locationHeader = response.headers['location'];
+ const location = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader;
if (!location) {
throw new Error('Redirect without Location header');
}
diff --git a/src/common/stellar-signature.ts b/src/common/stellar-signature.ts
index 63ab1c84..4aa25c4e 100644
--- a/src/common/stellar-signature.ts
+++ b/src/common/stellar-signature.ts
@@ -12,8 +12,6 @@
import { Keypair } from "@stellar/stellar-sdk";
import { UnauthorizedException } from "@nestjs/common";
import { createHash } from "node:crypto";
-import { Keypair } from "@stellar/stellar-sdk";
-import { UnauthorizedException } from "@nestjs/common";
export const INTENT_SIGNATURE_CLOCK_SKEW_SECONDS = 30;
export const MAX_INTENT_SIGNATURE_TTL_SECONDS = 900;
@@ -40,33 +38,6 @@ function buildV2IntentMessage(
return `vortex:${context.network}:${action}:${intentId}:${context.nonce}:${context.expiresAt}:${payloadHash}`;
}
-/**
- * Verify that `signature` (base64) over `message` (utf-8) was produced by
- * the private key corresponding to `publicKey` (Stellar G-address).
- *
- * Throws UnauthorizedException on any failure so callers can let it propagate
- * straight to the HTTP layer.
- */
-export function verifyStellarSignature(
- publicKey: string,
- message: string,
- signature: string,
-): void {
- try {
- const keypair = Keypair.fromPublicKey(publicKey);
- const messageBytes = Buffer.from(message, "utf8");
- const signatureBytes = Buffer.from(signature, "base64");
- if (!keypair.verify(messageBytes, signatureBytes)) {
- throw new UnauthorizedException("Invalid Stellar signature");
- }
- } catch (error) {
- if (error instanceof UnauthorizedException) {
- throw error;
- }
- throw new UnauthorizedException("Invalid Stellar signature");
- }
-}
-
/**
* Verify that `signature` (base64) over `message` (utf-8) was produced by
* the private key corresponding to `publicKey` (Stellar G-address).
@@ -102,8 +73,6 @@ export function buildCancelMessage(intentId: string, context?: IntentSignatureCo
return `cancel:${intentId}`;
}
- return `cancel:${intentId}`;
-}
/**
* Build the canonical message that an intent owner must sign to amend it.
@@ -124,6 +93,16 @@ export function buildWsAuthMessage(solver: string, timestamp: number | string):
return `solver-auth:${solver}:${String(timestamp)}`;
}
+/**
+ * Build the canonical message that a solver must sign to report liveness
+ * over REST (issue #445). The timestamp keeps the proof fresh: a heartbeat
+ * older than the offline window is rejected, so a captured signature cannot
+ * be replayed to keep a dead solver "live".
+ */
+export function buildHeartbeatMessage(address: string, timestamp: number | string): string {
+ return `heartbeat:${address}:${String(timestamp)}`;
+}
+
/**
* Build the canonical message that a solver must sign to accept an intent.
*/
@@ -132,8 +111,6 @@ export function buildAcceptMessage(intentId: string, solver: string, context?: I
return `accept:${intentId}:${solver}`;
}
- return `accept:${intentId}:${solver}`;
-}
/**
* Build the canonical message that a solver must sign to fill an intent.
@@ -153,8 +130,6 @@ export function buildFillMessage(
}
return `fill:${intentId}:${solver}`;
}
- return `fill:${intentId}:${solver}`;
-}
/**
* Build the canonical message that a solver must sign to register.
@@ -163,6 +138,14 @@ export function buildRegisterMessage(address: string): string {
return `register:${address}`;
}
+/**
+ * Canonical message a solver signs to prove a fill landed in time and cancel
+ * a pending slash during its challenge window (issue #397).
+ */
+export function buildFillProofMessage(intentId: string, solver: string, txHash: string): string {
+ return `fill-proof:${intentId}:${solver}:${txHash}`;
+}
+
/**
* Build the canonical message that a solver must sign to change status.
*/
@@ -203,16 +186,3 @@ export function buildDisputeReviewMessage(disputeId: string): string {
export function buildDisputeDecisionMessage(disputeId: string, resolution: string, reason: string): string {
return `dispute-decision:${disputeId}:${resolution}:${reason}`;
}
-
-/**
- * Build the canonical message that a solver must sign to update their mutable
- * profile fields (name / supportedChains / supportedTokens / avgFillTime).
- *
- * Signing over just the address is sufficient here: it proves control of the
- * account whose profile is being edited, and the request body is already
- * constrained by the DTO whitelist so no immutable field can ride along.
- */
-export function buildUpdateSolverMessage(address: string): string {
- return `update-solver:${address}`;
-}
-
diff --git a/src/common/validators/is-valid-address.validator.ts b/src/common/validators/is-valid-address.validator.ts
index cd299259..fed65fbc 100644
--- a/src/common/validators/is-valid-address.validator.ts
+++ b/src/common/validators/is-valid-address.validator.ts
@@ -1,25 +1,35 @@
import { registerDecorator, ValidationOptions, ValidationArguments } from "class-validator";
+import { SupportedChain } from "../../intents/intents.types";
-export function IsValidAddress(validationOptions?: ValidationOptions) {
+/** Validation options plus the chain whose address format should be enforced. */
+export interface IsValidAddressOptions extends ValidationOptions {
+ /**
+ * Chain whose address format to check. When omitted the decorator falls
+ * back to the sibling `srcChain` property of the object under validation.
+ */
+ chain?: SupportedChain;
+}
+
+export function IsValidAddress(validationOptions?: IsValidAddressOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: "isValidAddress",
target: object.constructor,
- propertyName: propertyName,
+ propertyName,
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
- const srcChain = (args.object as any).srcChain;
-
- if (srcChain === "stellar") {
+ const chain = validationOptions?.chain ?? (args.object as any).srcChain;
+
+ if (chain === "stellar") {
return typeof value === "string" && /^G[A-Z2-7]{55}$/.test(value);
}
-
+
return typeof value === "string" && /^0x[a-fA-F0-9]{40}$/.test(value);
},
defaultMessage(args: ValidationArguments) {
- const srcChain = (args.object as any).srcChain;
- if (srcChain === "stellar") {
+ const chain = validationOptions?.chain ?? (args.object as any).srcChain;
+ if (chain === "stellar") {
return "Stellar addresses must be 56 characters";
}
return "EVM addresses must be 42 characters starting with 0x";
diff --git a/src/config/configuration.ts b/src/config/configuration.ts
index 18648a68..c91c1f6c 100644
--- a/src/config/configuration.ts
+++ b/src/config/configuration.ts
@@ -1,338 +1,3 @@
-import { SupportedChain } from "../intents/intents.types";
-
-// ─── Chain deadline defaults ──────────────────────────────────────────────────
-
-/**
- * Chain-specific default intent deadlines (seconds from creation).
- * Stellar settles in ~5 s; EVM chains have longer finality windows.
- */
-export const CHAIN_DEADLINE_DEFAULTS: Partial> = {
- stellar: 300,
- ethereum: 1800,
- base: 900,
- polygon: 900,
- arbitrum: 900,
- optimism: 900,
- avalanche: 900,
-};
-
-export const DEFAULT_DEADLINE_SECONDS = 1800;
-
-/**
- * Chain-specific fill-window defaults (seconds from accept to fill deadline).
- * Shorter chains can fill faster.
- */
-export const CHAIN_FILL_WINDOW_DEFAULTS: Partial> = {
- stellar: 120,
- ethereum: 600,
- base: 300,
- polygon: 300,
- arbitrum: 300,
- optimism: 300,
- avalanche: 300,
-};
-
-export const DEFAULT_FILL_WINDOW_SECONDS = 600;
-
-// ─── AppConfig ────────────────────────────────────────────────────────────────
-
-export interface AppConfig {
- nodeEnv: "development" | "production" | "test";
- port: number;
- corsOrigin: string;
- databaseUrl: string;
- /** Comma-separated read-replica URLs (#411). Blank = primary only. */
- databaseReplicaUrls: string;
- /** Maximum replica lag (ms) before a replica is bypassed (#411). */
- maxReplicaLagMs: number;
-
- intentRetentionDays: number;
- intentRetentionSweepMs: number;
- onchainIntentsEnabled: boolean;
- onchainDryRun: boolean;
- intentsStore: "memory" | "dual" | "postgres";
- intentsVerifyIntervalMs: number;
-
- stellar: {
- network: "testnet" | "futurenet" | "mainnet";
- sorobanRpcUrl: string;
- sorobanRpcUrls: string;
- archivalRpcUrl: string;
- horizonUrl: string;
- signerSecretKey: string;
- settlementContractId: string;
- solverRegistryContractId: string;
- treasuryAddress: string;
- sorobanSigningKey: string;
- };
-
- ws: {
- maxConnections: number;
- backplane: string;
- maxPayloadBytes: number;
- maxConnectionsPerIp: number;
- trustProxyHops: number;
- rateLimitPerSec: number;
- rateLimitBurst: number;
- rateLimitMaxViolations: number;
- outboundQueueMax: number;
- outboundBufferBytes: number;
- slowConsumerPolicy: string;
- drainTimeoutMs: number;
- };
-
- sse: {
- heartbeatMs: number;
- maxBufferBytes: number;
- };
-
- redis: {
- url: string;
- };
-
- jobs: {
- driver: "memory" | "bullmq";
- shutdownTimeoutMs: number;
- processRole: "api" | "worker" | "all";
- };
-
- killswitch: {
- operatorToken: string;
- redisUrl: string;
- pollMs: number;
- persistence: "memory" | "prisma";
- };
-
- auth: {
- jwtSecret: string;
- };
-
- evmRpcUrls: Record;
- evmDepositVerificationEnabled: boolean;
- evmEscrowAddresses: Record;
- evmTransferFeeTolerance: number;
- evmLogLookbackBlocks: number;
-
- flags: {
- pubsub: "memory" | "redis";
- refreshMs: number;
- overrides: string;
- };
-
- shadow: {
- enabled: boolean;
- sampleRate: number;
- queueMax: number;
- concurrency: number;
- sourceAccount: string;
- };
-
- health: {
- checkIntervalMs: number;
- readyFailureThreshold: number;
- readySuccessThreshold: number;
- eventLoopMaxLagMs: number;
- serviceRoles: string;
- };
-
- governance: {
- paramsContractId: string;
- paramsPollIntervalMs: number;
- };
-
- leaderElection: {
- enabled: boolean;
- heartbeatMs: number;
- };
-
- metrics: {
- token: string;
- };
-
- sentry: {
- dsn: string;
- };
-
- log: {
- level: string;
- serviceName: string;
- shippingEnabled: boolean;
- shippingHost: string;
- shippingPort: number;
- shippingPath: string;
- shippingSsl: boolean;
- };
-
- // #413 — Cold-storage archival
- archival: {
- enabled: boolean;
- bucketName: string;
- endpoint: string;
- region: string;
- accessKeyId: string;
- secretAccessKey: string;
- retentionDays: number;
- partitionPrefix: string;
- maxRowsPerFile: number;
- };
-
- // Cursor HMAC secret (#412)
- cursorHmacSecret: string;
-}
-
-// ─── Factory function ─────────────────────────────────────────────────────────
-
-export default function configuration(): AppConfig {
- const e = process.env;
-
- const parseJson = (raw: string | undefined, fallback: T): T => {
- if (!raw) return fallback;
- try {
- return JSON.parse(raw) as T;
- } catch {
- return fallback;
- }
- };
-
- return {
- nodeEnv: (e.NODE_ENV ?? "development") as AppConfig["nodeEnv"],
- port: parseInt(e.PORT ?? "4000", 10),
- corsOrigin: e.CORS_ORIGIN ?? "*",
- databaseUrl: e.DATABASE_URL ?? "postgresql://vortex:vortex@localhost:5432/vortex?schema=public",
- databaseReplicaUrls: e.DATABASE_REPLICA_URLS ?? "",
- maxReplicaLagMs: parseInt(e.MAX_REPLICA_LAG_MS ?? "5000", 10),
-
- intentRetentionDays: parseInt(e.INTENT_RETENTION_DAYS ?? "30", 10),
- intentRetentionSweepMs: parseInt(e.INTENT_RETENTION_SWEEP_MS ?? "60000", 10),
- onchainIntentsEnabled: e.ONCHAIN_INTENTS_ENABLED === "true",
- onchainDryRun: e.ONCHAIN_DRY_RUN !== "false",
- intentsStore: (e.INTENTS_STORE ?? e.INTENTS_PERSISTENCE ?? "memory") as AppConfig["intentsStore"],
- intentsVerifyIntervalMs: parseInt(e.INTENTS_VERIFY_INTERVAL_MS ?? "60000", 10),
-
- stellar: {
- network: (e.STELLAR_NETWORK ?? "testnet") as AppConfig["stellar"]["network"],
- sorobanRpcUrl: e.SOROBAN_RPC_URL ?? "https://soroban-testnet.stellar.org",
- sorobanRpcUrls: e.SOROBAN_RPC_URLS ?? "",
- archivalRpcUrl: e.ARCHIVAL_RPC_URL ?? "",
- horizonUrl: e.HORIZON_URL ?? "https://horizon-testnet.stellar.org",
- signerSecretKey: e.STELLAR_SIGNER_SECRET_KEY ?? "",
- settlementContractId: e.SETTLEMENT_CONTRACT_ID ?? "",
- solverRegistryContractId: e.SOLVER_REGISTRY_CONTRACT_ID ?? "",
- treasuryAddress: e.TREASURY_ADDRESS ?? "",
- sorobanSigningKey: e.SOROBAN_SIGNING_KEY ?? "",
- },
-
- ws: {
- maxConnections: parseInt(e.WS_MAX_CONNECTIONS ?? "1000", 10),
- backplane: e.WS_BACKPLANE ?? "memory",
- maxPayloadBytes: parseInt(e.WS_MAX_PAYLOAD_BYTES ?? "16384", 10),
- maxConnectionsPerIp: parseInt(e.WS_MAX_CONNECTIONS_PER_IP ?? "20", 10),
- trustProxyHops: parseInt(e.WS_TRUST_PROXY_HOPS ?? "0", 10),
- rateLimitPerSec: parseInt(e.WS_RATE_LIMIT_PER_SEC ?? "10", 10),
- rateLimitBurst: parseInt(e.WS_RATE_LIMIT_BURST ?? "20", 10),
- rateLimitMaxViolations: parseInt(e.WS_RATE_LIMIT_MAX_VIOLATIONS ?? "5", 10),
- outboundQueueMax: parseInt(e.WS_OUTBOUND_QUEUE_MAX ?? "1000", 10),
- outboundBufferBytes: parseInt(e.WS_OUTBOUND_BUFFER_BYTES ?? "1048576", 10),
- slowConsumerPolicy: e.WS_SLOW_CONSUMER_POLICY ?? "drop_oldest",
- drainTimeoutMs: parseInt(e.WS_DRAIN_TIMEOUT_MS ?? "25000", 10),
- },
-
- sse: {
- heartbeatMs: parseInt(e.SSE_HEARTBEAT_MS ?? "15000", 10),
- maxBufferBytes: parseInt(e.SSE_MAX_BUFFER_BYTES ?? "1048576", 10),
- },
-
- redis: {
- url: e.REDIS_URL ?? "redis://localhost:6379",
- },
-
- jobs: {
- driver: (e.JOBS_DRIVER ?? "memory") as AppConfig["jobs"]["driver"],
- shutdownTimeoutMs: parseInt(e.JOBS_SHUTDOWN_TIMEOUT_MS ?? "25000", 10),
- processRole: (e.PROCESS_ROLE ?? "all") as AppConfig["jobs"]["processRole"],
- },
-
- killswitch: {
- operatorToken: e.KILLSWITCH_OPERATOR_TOKEN ?? "",
- redisUrl: e.KILLSWITCH_REDIS_URL ?? "",
- pollMs: parseInt(e.KILLSWITCH_POLL_MS ?? "2000", 10),
- persistence: (e.KILLSWITCH_PERSISTENCE ?? "memory") as AppConfig["killswitch"]["persistence"],
- },
-
- auth: {
- jwtSecret: e.AUTH_JWT_SECRET ?? "",
- },
-
- evmRpcUrls: parseJson>(e.EVM_RPC_URLS, {}),
- evmDepositVerificationEnabled: e.EVM_DEPOSIT_VERIFICATION_ENABLED === "true",
- evmEscrowAddresses: parseJson>(e.EVM_ESCROW_ADDRESSES, {}),
- evmTransferFeeTolerance: parseInt(e.EVM_TRANSFER_FEE_TOLERANCE_BPS ?? "0", 10),
- evmLogLookbackBlocks: parseInt(e.EVM_LOG_LOOKBACK_BLOCKS ?? "10000", 10),
-
- flags: {
- pubsub: (e.FLAGS_PUBSUB ?? "memory") as AppConfig["flags"]["pubsub"],
- refreshMs: parseInt(e.FLAGS_REFRESH_MS ?? "30000", 10),
- overrides: e.FLAG_OVERRIDES ?? "",
- },
-
- shadow: {
- enabled: e.SHADOW_MODE_ENABLED === "true",
- sampleRate: parseFloat(e.SHADOW_SAMPLE_RATE ?? "1"),
- queueMax: parseInt(e.SHADOW_QUEUE_MAX ?? "256", 10),
- concurrency: parseInt(e.SHADOW_CONCURRENCY ?? "4", 10),
- sourceAccount: e.SHADOW_SOURCE_ACCOUNT ?? "",
- },
-
- health: {
- checkIntervalMs: parseInt(e.HEALTH_CHECK_INTERVAL_MS ?? "5000", 10),
- readyFailureThreshold: parseInt(e.HEALTH_READY_FAILURE_THRESHOLD ?? "3", 10),
- readySuccessThreshold: parseInt(e.HEALTH_READY_SUCCESS_THRESHOLD ?? "2", 10),
- eventLoopMaxLagMs: parseInt(e.HEALTH_EVENT_LOOP_MAX_LAG_MS ?? "1000", 10),
- serviceRoles: e.SERVICE_ROLES ?? "api,ws,worker",
- },
-
- governance: {
- paramsContractId: e.PARAMS_CONTRACT_ID ?? "",
- paramsPollIntervalMs: parseInt(e.PARAMS_POLL_INTERVAL_MS ?? "30000", 10),
- },
-
- leaderElection: {
- enabled: e.LEADER_ELECTION_ENABLED === "true",
- heartbeatMs: parseInt(e.LEADER_ELECTION_HEARTBEAT_MS ?? "5000", 10),
- },
-
- metrics: {
- token: e.METRICS_TOKEN ?? "",
- },
-
- sentry: {
- dsn: e.SENTRY_DSN ?? "",
- },
-
- log: {
- level: e.LOG_LEVEL ?? "debug",
- serviceName: e.LOG_SERVICE_NAME ?? "vortex-backend",
- shippingEnabled: e.LOG_SHIPPING_ENABLED === "true",
- shippingHost: e.LOG_SHIPPING_HOST ?? "",
- shippingPort: parseInt(e.LOG_SHIPPING_PORT ?? "514", 10),
- shippingPath: e.LOG_SHIPPING_PATH ?? "/",
- shippingSsl: e.LOG_SHIPPING_SSL === "true",
- },
-
- archival: {
- enabled: e.ARCHIVAL_ENABLED === "true",
- bucketName: e.ARCHIVAL_BUCKET_NAME ?? "vortex-archives",
- endpoint: e.ARCHIVAL_S3_ENDPOINT ?? "",
- region: e.ARCHIVAL_S3_REGION ?? "us-east-1",
- accessKeyId: e.ARCHIVAL_S3_ACCESS_KEY_ID ?? "",
- secretAccessKey: e.ARCHIVAL_S3_SECRET_ACCESS_KEY ?? "",
- retentionDays: parseInt(e.ARCHIVAL_RETENTION_DAYS ?? "30", 10),
- partitionPrefix: e.ARCHIVAL_PARTITION_PREFIX ?? "date=",
- maxRowsPerFile: parseInt(e.ARCHIVAL_MAX_ROWS_PER_FILE ?? "100000", 10),
- },
-
- cursorHmacSecret: e.CURSOR_HMAC_SECRET ?? "dev-cursor-hmac-secret-do-not-use-in-prod",
- };
export type FeePercentile =
| "min"
| "mode"
@@ -430,10 +95,37 @@ export const NETWORK_PASSPHRASES: Record>;
+ /** Per-chain escrow contract address emitting `Deposited`. */
+ escrowAddresses: Partial>;
+ /**
+ * Maximum shortfall between the escrow's received amount and srcAmount, in
+ * basis points — accommodates fee-on-transfer tokens. 0 = exact.
+ */
+ transferFeeToleranceBps: number;
+ /** How far back to search for the Deposited log when no srcTxHash is given. */
+ logLookbackBlocks: number;
+}
+
export interface AppConfig {
nodeEnv: string;
port: number;
databaseUrl: string;
+ /** Comma-separated read-replica URLs (#411). Blank = primary only. */
+ databaseReplicaUrls: string;
+ /** Maximum replica lag (ms) before a replica is bypassed (#411). */
+ maxReplicaLagMs: number;
stellar: {
network: "testnet" | "futurenet" | "mainnet";
sorobanRpcUrl: string;
@@ -448,13 +140,24 @@ export interface AppConfig {
signingKey: string;
/** Fee percentile to use when estimating Soroban inclusion fees. */
feePercentile: FeePercentile;
+ /** Maximum fee in stroops for fee-bump escalation (#388). */
+ maxFeeStroops: number;
+ /** Number of channel accounts in the pool (#387). */
+ channelPoolSize: number;
+ /** Comma-separated list of channel account secret keys (#387). */
+ channelSecretKeys: string;
};
treasury: {
address: string;
};
onchainIntentsEnabled: boolean;
legacyStellarSignatures: boolean;
- evm: {
+ /**
+ * EVM-side configuration: the deposit-verification knobs (issues #402-#405)
+ * intersected with the signature-verification tables used by
+ * `EvmSignatureVerifier` (RPC allowlist + per-chain escrow addresses).
+ */
+ evm: EvmVerificationConfig & {
rpcAllowlist: string[];
chains: Record<
"ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche",
@@ -463,7 +166,10 @@ export interface AppConfig {
};
intentRetentionDays: number;
intentRetentionSweepMs: number;
- quoteAuctionWindowMs: number;
+ /** Dual-write consistency-verification sweep interval (issue #404). */
+ intentsVerifyIntervalMs: number;
+ /** Low-frequency sweeper that catches deadline jobs the queue did not run. */
+ safetySweepIntervalMs: number;
/**
* Dry-run flag for on-chain write paths (issue #260).
*
@@ -516,34 +222,7 @@ export interface AppConfig {
*/
pollMs: number;
};
-lane is never open.
- */
- operatorToken: string;
- /**
- * Redis URL used for cross-replica pause propagation. Empty falls back to
- * database polling only, which still meets the propagation budget.
- */
- redisUrl: string;
- /**
- * Interval (ms) for the `max_updated_at` probe that backstops Redis pub/sub.
- * Worst-case propagation delay is roughly this value, so it must stay
- * comfortably under the 5 s propagation requirement.
- */
- pollMs: number;
- };
- /**
- * Shadow-mode divergence monitor (issue #401).
- *
- * Runs read-only on-chain simulations of every intent state transition in
- * parallel with the authoritative off-chain path and reports where the two
- * disagree. See docs/runbooks/onchain-cutover.md for the go/no-go threshold.
- */
- shadow: {
- /** Master switch. When false, `ShadowService.observe` is a no-op. */
- enabled: boolean;
- /** Fraction of transitions to simulate, in `[0, 1]`. `1` = every one. */
- sampleR
/**
* Shadow-mode divergence monitor (issue #401).
*
@@ -585,48 +264,6 @@ lane is never open.
paramsPollIntervalMs: number;
};
- governance: {
- /**
- * On-chain governance / parameters contract ID.
- * When set, ProtocolParamsService reads current + scheduled parameters
- * from this contract and exposes them via GET /api/v1/params.
- * Leave blank to use code / env defaults only.
- */
- paramsContractId: string;
- /**
- * How often (in milliseconds) to poll the parameters contract for changes.
- * Default: 30 000 ms (30 s).
- */
- paramsPollIntervalMs: number;
- };
- governance: {
- /**
- * On-chain governance / parameters contract ID.
- * When set, ProtocolParamsService reads current + scheduled parameters
- * from this contract and exposes them via GET /api/v1/params.
- * Leave blank to use code / env defaults only.
- */
- paramsContractId: string;
- /**
- * How often (in milliseconds) to poll the parameters contract for changes.
- * Default: 30 000 ms (30 s).
- */
- paramsPollIntervalMs: number;
- };
- leaderElection: {
- /** When false, all workers run unconditionally (pre-election behaviour). */
- enabled: boolean;
- /** Heartbeat interval in ms (default 5000). */
- heartbeatMs: number;
- };
- /**
- * Process role (issue #494). Producers may enqueue jobs from any role;
- * queue workers only run when the role is "worker" or "all".
- */
- processRole: "api" | "worker" | "all";
- jobs: {
- /** "memory" (single-process, dev/test) or "bullmq" (Redis-backed, durable). */
- drive
leaderElection: {
/** When false, all workers run unconditionally (pre-election behaviour). */
enabled: boolean;
@@ -716,6 +353,14 @@ lane is never open.
* (issue #443): "memory" (single instance) or "redis" (pub/sub).
*/
credentialRevocationPubsub: "memory" | "redis";
+ /**
+ * Solver liveness heartbeats (issue #445): cadence the server expects
+ * clients to beat at, misses tolerated before auto-offline, and the
+ * shared store URL ("" → process-local, single replica).
+ */
+ solverHeartbeatIntervalMs: number;
+ solverHeartbeatMisses: number;
+ solverHeartbeatRedisUrl: string;
/** SSE intent feed (issue #433). */
sse: {
/** Heartbeat interval in milliseconds (SSE comment frames). */
@@ -738,6 +383,61 @@ lane is never open.
/** Soroban RPC endpoints probed for quorum (majority must be healthy). */
rpcHealthUrls: string[];
};
+ /** Solver reputation scoring (issue #444, RFC 0003). */
+ reputation: {
+ weights: {
+ fillRate: number;
+ latency: number;
+ slashes: number;
+ quoteHonour: number;
+ volume: number;
+ };
+ /** Exponential decay half-life in seconds. */
+ decayHalflifeSeconds: number;
+ /** Beta-distribution priors for the fill-rate Bayesian lower bound. */
+ bayesAlpha: number;
+ bayesBeta: number;
+ /** Volume-component knee in USD-equivalent notional (see RFC 0003). */
+ volumeLambdaUsd: number;
+ /** Trailing snapshot history exposed by /reputation (days, 1..365). */
+ historyWindowDays: number;
+ };
+ /** Transactional outbox relay (issue #454). */
+ outbox: {
+ /** Kill switch for the relay worker. Rows keep accumulating while false. */
+ relayEnabled: boolean;
+ relayIntervalMs: number;
+ batchSize: number;
+ /** Claims after which a row is moved to `dead` and alerted on. */
+ maxAttempts: number;
+ /**
+ * Processing lease. Must exceed the signed transaction's time bound so a
+ * reclaimed row whose envelope is NOT_FOUND can be safely resubmitted.
+ */
+ leaseSeconds: number;
+ };
+ /** On-chain slashing saga (issue #397). */
+ slashing: {
+ /** Delay between detection and broadcast during which a slash can be cancelled. */
+ challengeWindowSeconds: number;
+ /** Grace added to the fill deadline when judging whether a fill landed in time. */
+ clockSkewToleranceSeconds: number;
+ /** Failed submissions after which the slash is cancelled and compensated. */
+ maxSubmitAttempts: number;
+ };
+
+ // #413 — Cold-storage archival
+ archival: {
+ enabled: boolean;
+ bucketName: string;
+ endpoint: string;
+ region: string;
+ accessKeyId: string;
+ secretAccessKey: string;
+ retentionDays: number;
+ partitionPrefix: string;
+ maxRowsPerFile: number;
+ };
}
export default (): AppConfig => ({
@@ -746,6 +446,8 @@ export default (): AppConfig => ({
databaseUrl:
process.env.DATABASE_URL ??
"postgresql://vortex:vortex@localhost:5432/vortex?schema=public",
+ databaseReplicaUrls: process.env.DATABASE_REPLICA_URLS ?? "",
+ maxReplicaLagMs: parseInt(process.env.MAX_REPLICA_LAG_MS ?? "5000", 10),
stellar: {
network: (process.env.STELLAR_NETWORK ?? "testnet") as AppConfig["stellar"]["network"],
sorobanRpcUrl: process.env.SOROBAN_RPC_URL ?? "https://soroban-testnet.stellar.org",
@@ -755,6 +457,9 @@ export default (): AppConfig => ({
signerSecretKey: process.env.STELLAR_SIGNER_SECRET_KEY ?? "",
signingKey: process.env.SOROBAN_SIGNING_KEY ?? "",
feePercentile: (process.env.SOROBAN_FEE_PERCENTILE ?? "p50") as FeePercentile,
+ maxFeeStroops: parseInt(process.env.SOROBAN_MAX_FEE_STROOPS ?? "1000000", 10),
+ channelPoolSize: parseInt(process.env.CHANNEL_POOL_SIZE ?? "8", 10),
+ channelSecretKeys: process.env.CHANNEL_SECRET_KEYS ?? "",
},
treasury: {
address: process.env.TREASURY_ADDRESS ?? "",
@@ -772,10 +477,16 @@ export default (): AppConfig => ({
optimism: { chainId: 10, rpcUrl: process.env.OPTIMISM_RPC_URL ?? "", escrowAddress: process.env.OPTIMISM_ESCROW_ADDRESS ?? "" },
avalanche: { chainId: 43114, rpcUrl: process.env.AVALANCHE_RPC_URL ?? "", escrowAddress: process.env.AVALANCHE_ESCROW_ADDRESS ?? "" },
},
+ depositVerificationEnabled: (process.env.EVM_DEPOSIT_VERIFICATION_ENABLED ?? "false") === "true",
+ rpcUrls: parseJsonMap(process.env.EVM_RPC_URLS),
+ escrowAddresses: parseJsonMap(process.env.EVM_ESCROW_ADDRESSES),
+ transferFeeToleranceBps: parseInt(process.env.EVM_TRANSFER_FEE_TOLERANCE_BPS ?? "0", 10),
+ logLookbackBlocks: parseInt(process.env.EVM_LOG_LOOKBACK_BLOCKS ?? "10000", 10),
},
intentRetentionDays: parseInt(process.env.INTENT_RETENTION_DAYS ?? "30", 10),
intentRetentionSweepMs: parseInt(process.env.INTENT_RETENTION_SWEEP_MS ?? "60000", 10),
- quoteAuctionWindowMs: parseInt(process.env.QUOTE_AUCTION_WINDOW_MS ?? "300", 10),
+ intentsVerifyIntervalMs: parseInt(process.env.INTENTS_VERIFY_INTERVAL_MS ?? "60000", 10),
+ safetySweepIntervalMs: parseInt(process.env.SAFETY_SWEEP_INTERVAL_MS ?? "300000", 10),
// Default to dry-run (true) outside production; in production the value must
// be explicitly set (validated by envValidationSchema).
onchainDryRun: process.env.ONCHAIN_DRY_RUN !== undefined
@@ -874,6 +585,13 @@ export default (): AppConfig => ({
// to "" to force the bounded local limiter (single-replica / test).
rateLimitRedisUrl: process.env.RATE_LIMIT_REDIS_URL ?? process.env.REDIS_URL ?? "",
credentialRevocationPubsub: (process.env.CREDENTIAL_REVOCATION_PUBSUB ?? "memory") as "memory" | "redis",
+ // Solver liveness heartbeats (#445): client cadence (ms), tolerated misses
+ // (offline after interval × misses), and the shared liveness store URL.
+ // Defaults to REDIS_URL so multi-replica deployments share liveness for
+ // free; "" (no REDIS_URL) falls back to the process-local memory store.
+ solverHeartbeatIntervalMs: parseInt(process.env.SOLVER_HEARTBEAT_INTERVAL_MS ?? "10000", 10),
+ solverHeartbeatMisses: parseInt(process.env.SOLVER_HEARTBEAT_MISSES ?? "3", 10),
+ solverHeartbeatRedisUrl: process.env.SOLVER_HEARTBEAT_REDIS_URL ?? process.env.REDIS_URL ?? "",
sse: {
heartbeatMs: parseInt(process.env.SSE_HEARTBEAT_MS ?? "15000", 10),
maxBufferBytes: parseInt(process.env.SSE_MAX_BUFFER_BYTES ?? "1048576", 10),
@@ -892,8 +610,95 @@ export default (): AppConfig => ({
.map((u) => u.trim())
.filter(Boolean),
},
+ reputation: buildReputationConfig(process.env),
+ outbox: {
+ relayEnabled: (process.env.OUTBOX_RELAY_ENABLED ?? "true") === "true",
+ relayIntervalMs: parseInt(process.env.OUTBOX_RELAY_INTERVAL_MS ?? "2000", 10),
+ batchSize: parseInt(process.env.OUTBOX_RELAY_BATCH_SIZE ?? "10", 10),
+ maxAttempts: parseInt(process.env.OUTBOX_MAX_ATTEMPTS ?? "8", 10),
+ leaseSeconds: parseInt(process.env.OUTBOX_LEASE_SECONDS ?? "120", 10),
+ },
+ slashing: {
+ challengeWindowSeconds: parseInt(process.env.SLASH_CHALLENGE_WINDOW_SECONDS ?? "600", 10),
+ clockSkewToleranceSeconds: parseInt(process.env.SLASH_CLOCK_SKEW_TOLERANCE_SECONDS ?? "30", 10),
+ maxSubmitAttempts: parseInt(process.env.SLASH_MAX_SUBMIT_ATTEMPTS ?? "5", 10),
+ },
+
+ archival: {
+ enabled: process.env.ARCHIVAL_ENABLED === "true",
+ bucketName: process.env.ARCHIVAL_BUCKET_NAME ?? "vortex-archives",
+ endpoint: process.env.ARCHIVAL_S3_ENDPOINT ?? "",
+ region: process.env.ARCHIVAL_S3_REGION ?? "us-east-1",
+ accessKeyId: process.env.ARCHIVAL_S3_ACCESS_KEY_ID ?? "",
+ secretAccessKey: process.env.ARCHIVAL_S3_SECRET_ACCESS_KEY ?? "",
+ retentionDays: parseInt(process.env.ARCHIVAL_RETENTION_DAYS ?? "30", 10),
+ partitionPrefix: process.env.ARCHIVAL_PARTITION_PREFIX ?? "date=",
+ maxRowsPerFile: parseInt(process.env.ARCHIVAL_MAX_ROWS_PER_FILE ?? "100000", 10),
+ },
});
+/**
+ * Parse a JSON object of string values from an env var, falling back to an
+ * empty map for anything unparseable or non-object (keeps a typo in
+ * `EVM_RPC_URLS` from crashing boot; the verifier then reports per-chain
+ * "no RPC URL" failures instead).
+ */
+export function parseJsonMap(raw: string | undefined): Record {
+ if (!raw) return {};
+ try {
+ const parsed: unknown = JSON.parse(raw);
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
+ return Object.fromEntries(
+ Object.entries(parsed as Record).filter(
+ (entry): entry is [string, string] => typeof entry[1] === "string",
+ ),
+ );
+ } catch {
+ return {};
+ }
+}
+
+/**
+ * Parse and validate reputation weights + knobs.
+ *
+ * In dev/test, weights are renormalised on the fly so deployers can tweak a
+ * single weight and get a working system. In production, env.validation.ts
+ * already fails the boot when the sum is off by more than 1e-9, so the
+ * renormalisation branch below is effectively a no-op.
+ */
+function buildReputationConfig(env: NodeJS.ProcessEnv): AppConfig["reputation"] {
+ const fillRate = Number(env.REP_WEIGHT_FILL_RATE ?? 0.35);
+ const latency = Number(env.REP_WEIGHT_LATENCY ?? 0.15);
+ const slashes = Number(env.REP_WEIGHT_SLASHES ?? 0.25);
+ const quoteHonour = Number(env.REP_WEIGHT_QUOTE_HONOUR ?? 0.15);
+ const volume = Number(env.REP_WEIGHT_VOLUME ?? 0.10);
+ const sum = fillRate + latency + slashes + quoteHonour + volume;
+ let w = { fillRate, latency, slashes, quoteHonour, volume };
+ if (sum > 0 && Math.abs(sum - 1) > 1e-9) {
+ w = {
+ fillRate: fillRate / sum,
+ latency: latency / sum,
+ slashes: slashes / sum,
+ quoteHonour: quoteHonour / sum,
+ volume: volume / sum,
+ };
+ }
+ return {
+ weights: w,
+ decayHalflifeSeconds:
+ clampPositiveInt(env.REP_DECAY_HALFLIFE_SECONDS, 30 * 24 * 60 * 60),
+ bayesAlpha: Math.max(0.5, Number(env.REP_BAYES_ALPHA ?? 4)),
+ bayesBeta: Math.max(0.5, Number(env.REP_BAYES_BETA ?? 1)),
+ volumeLambdaUsd: Math.max(1, Number(env.REP_VOLUME_LAMBDA_USD ?? 100000)),
+ historyWindowDays: (() => {
+ const raw = parseInt(env.REP_HISTORY_WINDOW_DAYS ?? "30", 10);
+ if (!Number.isFinite(raw) || raw < 1) return 30;
+ if (raw > 365) return 365;
+ return raw;
+ })(),
+ };
+}
+
/** Parse `SHADOW_SAMPLE_RATE` into a probability, defaulting to full sampling. */
function clampSampleRate(raw: string | undefined): number {
if (raw === undefined || raw.trim() === "") return 1;
diff --git a/src/config/env.validation.ts b/src/config/env.validation.ts
index 7d4c88ec..ed5c21b3 100644
--- a/src/config/env.validation.ts
+++ b/src/config/env.validation.ts
@@ -1,263 +1,3 @@
-import Joi from "joi";
-
-/**
- * Joi schema for environment variables.
- *
- * Rules:
- * - Booleans accept "true"/"false" strings (env vars are always strings).
- * - Production secrets are required when NODE_ENV=production.
- * - New env vars for issues #411 (replica URLs), #412 (cursor secret),
- * and #413 (archival) are added here.
- */
-
-export const envValidationSchema = Joi.object({
- // ── Core ───────────────────────────────────────────────────────────────────
- NODE_ENV: Joi.string().valid("development", "production", "test").default("development"),
- PORT: Joi.number().integer().min(1).max(65535).default(4000),
- CORS_ORIGIN: Joi.string().default("*"),
-
- // ── Database ───────────────────────────────────────────────────────────────
- DATABASE_URL: Joi.string().default("postgresql://vortex:vortex@localhost:5432/vortex?schema=public"),
-
- /**
- * #411 — Read-replica URLs.
- * Comma-separated list of Postgres connection strings for read replicas.
- * Leave blank to use the primary for all reads.
- */
- DATABASE_REPLICA_URLS: Joi.string().allow("").default(""),
- MAX_REPLICA_LAG_MS: Joi.number().integer().min(100).max(60000).default(5000),
-
- // ── Stellar ────────────────────────────────────────────────────────────────
- STELLAR_NETWORK: Joi.string().valid("testnet", "futurenet", "mainnet").default("testnet"),
- SOROBAN_RPC_URL: Joi.string().uri().default("https://soroban-testnet.stellar.org"),
- SOROBAN_RPC_URLS: Joi.string().allow("").default(""),
- ARCHIVAL_RPC_URL: Joi.string().allow("").default(""),
- HORIZON_URL: Joi.string().uri().default("https://horizon-testnet.stellar.org"),
- STELLAR_SIGNER_SECRET_KEY: Joi.string().allow("").default(""),
- SETTLEMENT_CONTRACT_ID: Joi.string().allow("").default(""),
- SOLVER_REGISTRY_CONTRACT_ID: Joi.string().allow("").default(""),
- TREASURY_ADDRESS: Joi.string().allow("").default(""),
-
- SOROBAN_SIGNING_KEY: Joi.when("NODE_ENV", {
- is: "production",
- then: Joi.string()
- .pattern(/^S[A-Z2-7]{55}$/, "Stellar secret seed (S + 55 base32 chars)")
- .required(),
- otherwise: Joi.string()
- .pattern(/^(S[A-Z2-7]{55})?$/, "Stellar secret seed or empty")
- .allow("")
- .default(""),
- }),
-
- SIGNER_BACKEND: Joi.string().valid("local", "vault").default("local"),
- VAULT_ADDR: Joi.string().allow("").default(""),
- VAULT_TOKEN: Joi.string().allow("").default(""),
- VAULT_TRANSIT_KEY_NAME: Joi.string().default("vortex-signer"),
- ALLOW_LOCAL_SIGNER_IN_PROD: Joi.boolean().default(false),
-
- // ── On-chain writes ────────────────────────────────────────────────────────
- ONCHAIN_INTENTS_ENABLED: Joi.boolean().default(false),
- ONCHAIN_DRY_RUN: Joi.when("NODE_ENV", {
- is: "production",
- then: Joi.boolean().required(),
- otherwise: Joi.boolean().default(true),
- }),
- SOROBAN_FEE_PERCENTILE: Joi.string()
- .valid("min", "mode", "p10", "p20", "p30", "p40", "p50", "p60", "p70", "p80", "p90", "p95", "p99", "max")
- .default("p50"),
- SOROBAN_MAX_FEE_STROOPS: Joi.number().integer().min(0).default(1_000_000),
- CHANNEL_POOL_SIZE: Joi.number().integer().min(1).default(8),
- CHANNEL_SECRET_KEYS: Joi.string().allow("").default(""),
-
- // ── Persistence ────────────────────────────────────────────────────────────
- INTENTS_STORE: Joi.string().valid("memory", "dual", "postgres").default("memory"),
- INTENTS_PERSISTENCE: Joi.string().valid("memory", "dual", "postgres").default("memory"),
- INTENTS_VERIFY_INTERVAL_MS: Joi.number().integer().min(0).default(60000),
- SOLVERS_PERSISTENCE: Joi.string().valid("memory", "prisma").default("memory"),
- TOKENS_PERSISTENCE: Joi.string().valid("memory", "prisma").default("memory"),
-
- // ── Intent retention ───────────────────────────────────────────────────────
- INTENT_RETENTION_DAYS: Joi.number().integer().min(0).default(30),
- INTENT_RETENTION_SWEEP_MS: Joi.number().integer().min(0).default(60000),
-
- // ── WebSocket ──────────────────────────────────────────────────────────────
- WS_MAX_CONNECTIONS: Joi.number().integer().min(0).default(1000),
- WS_BACKPLANE: Joi.string().valid("memory", "redis").default("memory"),
- WS_MAX_PAYLOAD_BYTES: Joi.number().integer().min(1024).default(16384),
- WS_MAX_CONNECTIONS_PER_IP: Joi.number().integer().min(0).default(20),
- WS_TRUST_PROXY_HOPS: Joi.number().integer().min(0).default(0),
- WS_RATE_LIMIT_PER_SEC: Joi.number().integer().min(0).default(10),
- WS_RATE_LIMIT_BURST: Joi.number().integer().min(0).default(20),
- WS_RATE_LIMIT_MAX_VIOLATIONS: Joi.number().integer().min(0).default(5),
- WS_OUTBOUND_QUEUE_MAX: Joi.number().integer().min(0).default(1000),
- WS_OUTBOUND_BUFFER_BYTES: Joi.number().integer().min(0).default(1048576),
- WS_SLOW_CONSUMER_POLICY: Joi.string().valid("drop_oldest", "disconnect").default("drop_oldest"),
- WS_DRAIN_TIMEOUT_MS: Joi.number().integer().min(0).default(25000),
-
- // ── SSE ────────────────────────────────────────────────────────────────────
- SSE_HEARTBEAT_MS: Joi.number().integer().min(0).default(15000),
- SSE_MAX_BUFFER_BYTES: Joi.number().integer().min(0).default(1048576),
-
- // ── Redis ──────────────────────────────────────────────────────────────────
- REDIS_URL: Joi.string().allow("").default("redis://localhost:6379"),
-
- // ── Jobs ───────────────────────────────────────────────────────────────────
- JOBS_DRIVER: Joi.string().valid("memory", "bullmq").default("memory"),
- JOBS_SHUTDOWN_TIMEOUT_MS: Joi.number().integer().min(0).default(25000),
- PROCESS_ROLE: Joi.string().valid("api", "worker", "all").default("all"),
-
- // ── Kill-switch ────────────────────────────────────────────────────────────
- KILLSWITCH_OPERATOR_TOKEN: Joi.when("NODE_ENV", {
- is: "production",
- then: Joi.string().min(1).required(),
- otherwise: Joi.string().allow("").default(""),
- }),
- KILLSWITCH_REDIS_URL: Joi.string().allow("").default(""),
- KILLSWITCH_POLL_MS: Joi.number().integer().min(100).max(5000).default(2000),
- KILLSWITCH_PERSISTENCE: Joi.string().valid("memory", "prisma").default("memory"),
-
- // ── Auth ───────────────────────────────────────────────────────────────────
- AUTH_JWT_SECRET: Joi.string().allow("").default(""),
- ADMIN_API_KEYS: Joi.string().allow("").default(""),
-
- // ── EVM ────────────────────────────────────────────────────────────────────
- ETHEREUM_RPC_URL: Joi.string().allow("").default(""),
- ETHEREUM_ESCROW_ADDRESS: Joi.string().allow("").default(""),
- BASE_RPC_URL: Joi.string().allow("").default(""),
- BASE_ESCROW_ADDRESS: Joi.string().allow("").default(""),
- POLYGON_RPC_URL: Joi.string().allow("").default(""),
- POLYGON_ESCROW_ADDRESS: Joi.string().allow("").default(""),
- ARBITRUM_RPC_URL: Joi.string().allow("").default(""),
- ARBITRUM_ESCROW_ADDRESS: Joi.string().allow("").default(""),
- OPTIMISM_RPC_URL: Joi.string().allow("").default(""),
- OPTIMISM_ESCROW_ADDRESS: Joi.string().allow("").default(""),
- AVALANCHE_RPC_URL: Joi.string().allow("").default(""),
- AVALANCHE_ESCROW_ADDRESS: Joi.string().allow("").default(""),
- EVM_RPC_ALLOWLIST: Joi.string().allow("").default(""),
- EVM_RPC_URLS: Joi.string().allow("").default("{}"),
- EVM_ESCROW_ADDRESSES: Joi.string().allow("").default("{}"),
- EVM_DEPOSIT_VERIFICATION_ENABLED: Joi.boolean().default(false),
- EVM_TRANSFER_FEE_TOLERANCE_BPS: Joi.number().integer().min(0).default(0),
- EVM_LOG_LOOKBACK_BLOCKS: Joi.number().integer().min(0).default(10000),
-
- // ── Resource limits ────────────────────────────────────────────────────────
- JSON_MAX_DEPTH: Joi.number().integer().min(1).max(100).default(10),
- WS_MAX_FILTER_CHAINS: Joi.number().integer().min(1).default(20),
- WS_MAX_SUBSCRIPTIONS: Joi.number().integer().min(1).default(10),
- DB_QUERY_TIMEOUT_MS: Joi.number().integer().min(0).default(5000),
- DB_BATCH_QUERY_TIMEOUT_MS: Joi.number().integer().min(0).default(10000),
- DB_STATS_QUERY_TIMEOUT_MS: Joi.number().integer().min(0).default(15000),
-
- // ── Flags ──────────────────────────────────────────────────────────────────
- FLAGS_PUBSUB: Joi.string().valid("memory", "redis").default("memory"),
- FLAGS_REFRESH_MS: Joi.number().integer().min(0).default(30000),
- FLAG_OVERRIDES: Joi.string().allow("").default(""),
-
- // ── Shadow mode ────────────────────────────────────────────────────────────
- SHADOW_MODE_ENABLED: Joi.boolean().default(false),
- SHADOW_SAMPLE_RATE: Joi.number().min(0).max(1).default(1),
- SHADOW_QUEUE_MAX: Joi.number().integer().min(0).default(256),
- SHADOW_CONCURRENCY: Joi.number().integer().min(1).default(4),
- SHADOW_SOURCE_ACCOUNT: Joi.string().allow("").default(""),
-
- // ── Health ─────────────────────────────────────────────────────────────────
- HEALTH_CHECK_INTERVAL_MS: Joi.number().integer().min(100).default(5000),
- HEALTH_READY_FAILURE_THRESHOLD: Joi.number().integer().min(1).default(3),
- HEALTH_READY_SUCCESS_THRESHOLD: Joi.number().integer().min(1).default(2),
- HEALTH_EVENT_LOOP_MAX_LAG_MS: Joi.number().integer().min(100).default(1000),
- SERVICE_ROLES: Joi.string().default("api,ws,worker"),
-
- // ── Governance ─────────────────────────────────────────────────────────────
- PARAMS_CONTRACT_ID: Joi.string().allow("").default(""),
- PARAMS_POLL_INTERVAL_MS: Joi.number().integer().min(0).default(30000),
-
- // ── Leader election ────────────────────────────────────────────────────────
- LEADER_ELECTION_ENABLED: Joi.boolean().default(false),
- LEADER_ELECTION_HEARTBEAT_MS: Joi.number().integer().min(0).default(5000),
-
- // ── Observability ──────────────────────────────────────────────────────────
- LOG_LEVEL: Joi.string().valid("error", "warn", "info", "http", "verbose", "debug", "silly").default("debug"),
- LOG_SERVICE_NAME: Joi.string().default("vortex-backend"),
- SENTRY_DSN: Joi.string().allow("").default(""),
- METRICS_TOKEN: Joi.string().allow("").default(""),
- LOG_SHIPPING_ENABLED: Joi.boolean().default(false),
- LOG_SHIPPING_HOST: Joi.string().allow("").default(""),
- LOG_SHIPPING_PORT: Joi.number().integer().min(1).max(65535).default(514),
- LOG_SHIPPING_PATH: Joi.string().default("/"),
- LOG_SHIPPING_SSL: Joi.boolean().default(false),
-
- // ── Reconciler ─────────────────────────────────────────────────────────────
- RECONCILE_STALE_SECONDS: Joi.number().integer().min(0).default(300),
-
- // ── Misc ───────────────────────────────────────────────────────────────────
- CANARY_ADDRESSES: Joi.string().allow("").default(""),
- ALLOW_LEGACY_STELLAR_SIGNATURES: Joi.boolean().default(false),
- SAFETY_SWEEP_INTERVAL_MS: Joi.number().integer().min(0).default(300000),
- RATE_LIMIT_LOCAL_PRUNE_MS: Joi.number().integer().min(0).default(60000),
- RATE_LIMIT_REDIS_URL: Joi.string().allow("").default(""),
- CREDENTIAL_REVOCATION_PUBSUB: Joi.string().valid("memory", "redis").default("memory"),
- GUARDIAN_CONTRACT_ID: Joi.string().allow("").default(""),
- DATASETS_ENABLED: Joi.boolean().default(false),
- DATASETS_ANONYMIZE: Joi.boolean().default(true),
- DATASETS_SALT: Joi.string().allow("").default(""),
- DATASETS_SALT_ROTATION_HOURS: Joi.number().integer().min(1).default(24),
- DATASETS_SALT_RETENTION_WINDOWS: Joi.number().integer().min(1).default(2),
- DATASETS_PUBLIC_BUCKET: Joi.string().allow("").default("vortex-public-datasets"),
- DATASETS_STORAGE_KIND: Joi.string().valid("memory", "local").default("memory"),
- DATASETS_LOCAL_DIR: Joi.string().allow("").default("./data/datasets"),
- SECRETS_PROVIDER: Joi.string().valid("env", "aws-secrets-manager", "vault-kv").default("env"),
- SECRETS_REFRESH_INTERVAL_MS: Joi.number().integer().min(0).default(60000),
- SECRETS_EXTRA: Joi.string().allow("").default(""),
- AWS_SECRETS_MANAGER_PREFIX: Joi.string().allow("").default(""),
- AWS_SECRETS_MANAGER_POLL_INTERVAL_MS: Joi.number().integer().min(0).default(60000),
- VAULT_KV_MOUNT: Joi.string().default("secret"),
- VAULT_KV_PREFIX: Joi.string().default("vortex/"),
- VAULT_KV_POLL_INTERVAL_MS: Joi.number().integer().min(0).default(60000),
- JWT_SIGNING_KEY: Joi.string().allow("").default(""),
- WEBHOOK_SECRET: Joi.string().allow("").default(""),
- CHANNEL_KEY: Joi.string().allow("").default(""),
- EGRESS_TIMEOUT_MS: Joi.number().integer().min(0).default(10000),
- EGRESS_MAX_REDIRECTS: Joi.number().integer().min(0).default(3),
- EGRESS_MAX_BODY_SIZE_BYTES: Joi.number().integer().min(0).default(10485760),
- SOROBAN_RPC_ALLOWLIST: Joi.string().allow("").default("soroban-testnet.stellar.org,soroban-rpc.stellar.org"),
- WEBHOOK_ALLOWLIST: Joi.string().allow("").default(""),
- ORACLE_ALLOWLIST: Joi.string().allow("").default(""),
- MAX_USER_SLIPPAGE_BPS: Joi.number().integer().min(0).default(100),
- MAX_PREMIUM_BPS: Joi.number().integer().min(0).default(50),
- ORACLE_FAIL_OPEN_MAX_USD: Joi.number().min(0).default(100),
- ORACLE_MAX_STALENESS_MS: Joi.number().integer().min(0).default(60000),
- OUTBOX_RELAY_ENABLED: Joi.boolean().default(true),
- OUTBOX_RELAY_INTERVAL_MS: Joi.number().integer().min(0).default(2000),
- OUTBOX_RELAY_BATCH_SIZE: Joi.number().integer().min(1).default(10),
- OUTBOX_MAX_ATTEMPTS: Joi.number().integer().min(1).default(8),
- OUTBOX_LEASE_SECONDS: Joi.number().integer().min(0).default(120),
- SLASH_CHALLENGE_WINDOW_SECONDS: Joi.number().integer().min(0).default(600),
- SLASH_CLOCK_SKEW_TOLERANCE_SECONDS: Joi.number().integer().min(0).default(30),
- SLASH_MAX_SUBMIT_ATTEMPTS: Joi.number().integer().min(1).default(5),
- SOLVER_SECRET: Joi.string().allow("").default(""),
- SOLVER_ADDRESS: Joi.string().allow("").default(""),
- SOLVER_CHAINS: Joi.string().default("stellar,ethereum,base,polygon,arbitrum,optimism,avalanche"),
-
- // ── #411 — Read replicas ───────────────────────────────────────────────────
- // DATABASE_REPLICA_URLS and MAX_REPLICA_LAG_MS already defined above.
-
- // ── #412 — Cursor HMAC secret ──────────────────────────────────────────────
- CURSOR_HMAC_SECRET: Joi.string().allow("").default("dev-cursor-hmac-secret-do-not-use-in-prod"),
-
- // ── #413 — Cold-storage archival ───────────────────────────────────────────
- ARCHIVAL_ENABLED: Joi.boolean().default(false),
- ARCHIVAL_BUCKET_NAME: Joi.string().default("vortex-archives"),
- ARCHIVAL_S3_ENDPOINT: Joi.string().allow("").default(""),
- ARCHIVAL_S3_REGION: Joi.string().default("us-east-1"),
- ARCHIVAL_S3_ACCESS_KEY_ID: Joi.string().allow("").default(""),
- ARCHIVAL_S3_SECRET_ACCESS_KEY: Joi.string().allow("").default(""),
- ARCHIVAL_RETENTION_DAYS: Joi.number().integer().min(1).default(30),
- ARCHIVAL_PARTITION_PREFIX: Joi.string().default("date="),
- ARCHIVAL_MAX_ROWS_PER_FILE: Joi.number().integer().min(1000).default(100000),
-}).options({ allowUnknown: true });
-
-// Re-export for consumers that need the inferred type.
-export type EnvConfig = ReturnType["value"];
import * as Joi from "joi";
// Stellar secret seeds ("S..." strkeys) are 56-char base32: prefix + 32-byte
@@ -284,6 +24,13 @@ export const envValidationSchema = Joi.object({
.uri({ scheme: ["postgresql", "postgres"] })
.default("postgresql://vortex:vortex@localhost:5432/vortex?schema=public"),
+ // ── #411 — Read replicas ─────────────────────────────────────────────────
+ // Comma-separated Postgres connection strings for read replicas.
+ // Leave blank to route all reads to the primary.
+ DATABASE_REPLICA_URLS: Joi.string().allow("").default(""),
+ // Maximum replica replication lag (ms) before a replica is bypassed.
+ MAX_REPLICA_LAG_MS: Joi.number().integer().min(100).max(60000).default(5000),
+
STELLAR_NETWORK: Joi.string().valid("testnet", "futurenet", "mainnet").default("testnet"),
SOROBAN_RPC_URL: Joi.string().uri().default("https://soroban-testnet.stellar.org"),
// Horizon base URL, used for account/balance reads (treasury, canary tooling).
@@ -312,6 +59,21 @@ export const envValidationSchema = Joi.object({
ONCHAIN_INTENTS_ENABLED: Joi.boolean().default(false),
// Stellar public key of the treasury account (fee/slash/refund accumulator).
TREASURY_ADDRESS: Joi.string().allow("").default(""),
+
+ ALLOW_LEGACY_STELLAR_SIGNATURES: Joi.boolean().default(false),
+ ETHEREUM_RPC_URL: Joi.string().uri({ scheme: ["http", "https"] }).allow("").default(""),
+ ETHEREUM_ESCROW_ADDRESS: Joi.string().pattern(/^$|^0x[a-fA-F0-9]{40}$/).default(""),
+ BASE_RPC_URL: Joi.string().uri({ scheme: ["http", "https"] }).allow("").default(""),
+ BASE_ESCROW_ADDRESS: Joi.string().pattern(/^$|^0x[a-fA-F0-9]{40}$/).default(""),
+ POLYGON_RPC_URL: Joi.string().uri({ scheme: ["http", "https"] }).allow("").default(""),
+ POLYGON_ESCROW_ADDRESS: Joi.string().pattern(/^$|^0x[a-fA-F0-9]{40}$/).default(""),
+ ARBITRUM_RPC_URL: Joi.string().uri({ scheme: ["http", "https"] }).allow("").default(""),
+ ARBITRUM_ESCROW_ADDRESS: Joi.string().pattern(/^$|^0x[a-fA-F0-9]{40}$/).default(""),
+ OPTIMISM_RPC_URL: Joi.string().uri({ scheme: ["http", "https"] }).allow("").default(""),
+ OPTIMISM_ESCROW_ADDRESS: Joi.string().pattern(/^$|^0x[a-fA-F0-9]{40}$/).default(""),
+ AVALANCHE_RPC_URL: Joi.string().uri({ scheme: ["http", "https"] }).allow("").default(""),
+ AVALANCHE_ESCROW_ADDRESS: Joi.string().pattern(/^$|^0x[a-fA-F0-9]{40}$/).default(""),
+ EVM_RPC_ALLOWLIST: Joi.string().allow("").default(""),
CORS_ORIGIN: Joi.string().default("*"),
WS_MAX_CONNECTIONS: Joi.number().integer().min(0).default(1000),
SOROBAN_FEE_PERCENTILE: Joi.string()
@@ -336,10 +98,11 @@ export const envValidationSchema = Joi.object({
WS_BACKPLANE: Joi.string().valid("memory", "redis").default("memory"),
REDIS_URL: Joi.string().uri({ scheme: ["redis", "rediss"] }).default("redis://localhost:6379"),
- // ── WebSocket replay store (issue #457) ──────────────────────────────────────
+ // ── WebSocket replay store (issue #457) ───────────────────────────────────
+ // WS_REPLAY_MAX_AGE_MS stays optional-by-omission (commented in the
+ // .env examples) and is read directly where the buffer is built.
WS_REPLAY_STORE: Joi.string().valid("memory", "redis").default("memory"),
WS_REPLAY_MAX_COUNT: Joi.number().integer().min(1).default(500),
- WS_REPLAY_MAX_AGE_MS: Joi.number().integer().min(1).optional(),
// ── Persistence adapter selection ─────────────────────────────────────────
// Controls which repository adapter is used for intents and solvers.
@@ -367,6 +130,22 @@ export const envValidationSchema = Joi.object({
// Sentry DSN for error alerting. Omit (or leave blank) to disable Sentry.
SENTRY_DSN: Joi.string().uri().allow("").default(""),
+ // Bearer token that guards GET /metrics (issue #298).
+ // When set, Prometheus scrape jobs must supply:
+ // Authorization: Bearer
+ // When empty (the default):
+ // - non-production: unauthenticated scraping allowed (local dev Prometheus)
+ // - production: rejected at boot — #298 requires a non-empty token of at
+ // least 16 characters so a deploy can never expose /metrics without
+ // authentication (fail closed at validation time, not at scrape time).
+ // Generate with: openssl rand -hex 32
+ METRICS_TOKEN: Joi.string()
+ .when("NODE_ENV", {
+ is: Joi.valid("production"),
+ then: Joi.string().required().invalid("").min(16),
+ otherwise: Joi.string().allow("").default(""),
+ }),
+
// Winston log level. Defaults to "debug" in dev/test and "info" in production.
LOG_LEVEL: Joi.string()
.valid("error", "warn", "info", "http", "verbose", "debug", "silly")
@@ -420,6 +199,7 @@ export const envValidationSchema = Joi.object({
// Must be explicitly set to "true" — any other value is treated as false.
// A startup warning is emitted when this is enabled in production.
ALLOW_LOCAL_SIGNER_IN_PROD: Joi.boolean().default(false),
+
// ── Resource-exhaustion limits (issue #476) ───────────────────────────────
// These values are consumed by src/config/limits.config.ts at startup and
// override the compile-time defaults when set. All have safe defaults so
@@ -542,6 +322,7 @@ export const envValidationSchema = Joi.object({
// Optional: when empty the monitor reports `contract_unconfigured` rather
// than silently recording zero divergence.
SHADOW_SOURCE_ACCOUNT: Joi.string().allow("").default(""),
+
// ── Governance parameters contract ────────────────────────────────────────
// When set, ProtocolParamsService reads current + scheduled protocol
// parameters (fee bps, fill windows, deadlines, exposure ratio, slash
@@ -552,6 +333,7 @@ export const envValidationSchema = Joi.object({
// How often (ms) to poll the parameters contract. 30 s is the default;
// lower values increase RPC load; raise in production if rate-limited.
PARAMS_POLL_INTERVAL_MS: Joi.number().integer().min(5_000).default(30_000),
+
// ── Leader election (issue #493) ──────────────────────────────────────────
// Controls whether Postgres advisory-lock based leader election is enabled
// for singleton workers (sweeper, event-ingestion).
@@ -597,6 +379,11 @@ export const envValidationSchema = Joi.object({
.pattern(/^([A-Za-z0-9_.-]+:(admin|superadmin):[^,:]{16,})(,[A-Za-z0-9_.-]+:(admin|superadmin):[^,:]{16,})*$/)
.default(""),
+ // ── Public anonymised datasets ────────────────────────────────────────────
+ // Legacy storage-backend selector (superseded by DATASETS_STORAGE below,
+ // still exported by the .env*.example files).
+ DATASETS_STORAGE_KIND: Joi.string().valid("local", "memory").default("memory"),
+
// ── Guardian emergency ingestion (issue #507) ─────────────────────────────
GUARDIAN_CONTRACT_ID: Joi.string().allow("").default(""),
@@ -635,4 +422,230 @@ export const envValidationSchema = Joi.object({
DATASETS_PUBLIC_BUCKET: Joi.string().default("vortex-public-datasets"),
DATASETS_STORAGE: Joi.string().valid("local", "memory").default("local"),
DATASETS_LOCAL_DIR: Joi.string().default(".datasets"),
+
+ // ── #412 — Cursor HMAC secret ──────────────────────────────────────────────
+ // HMAC-SHA256 key used to sign opaque pagination cursors.
+ // Generate with: openssl rand -hex 32
+ CURSOR_HMAC_SECRET: Joi.string().allow("").default("dev-cursor-hmac-secret-do-not-use-in-prod"),
+
+ // ── #413 — Cold-storage archival ───────────────────────────────────────────
+ ARCHIVAL_ENABLED: Joi.boolean().default(false),
+ ARCHIVAL_BUCKET_NAME: Joi.string().default("vortex-archives"),
+ ARCHIVAL_S3_ENDPOINT: Joi.string().allow("").default(""),
+ ARCHIVAL_S3_REGION: Joi.string().default("us-east-1"),
+ ARCHIVAL_S3_ACCESS_KEY_ID: Joi.string().allow("").default(""),
+ ARCHIVAL_S3_SECRET_ACCESS_KEY: Joi.string().allow("").default(""),
+ ARCHIVAL_RETENTION_DAYS: Joi.number().integer().min(1).default(30),
+ ARCHIVAL_PARTITION_PREFIX: Joi.string().default("date="),
+ ARCHIVAL_MAX_ROWS_PER_FILE: Joi.number().integer().min(1000).default(100000),
+ // ── Secrets Manager (issue #465) ────────────────────────────────────────────
+ SECRETS_PROVIDER: Joi.string().valid("env", "aws-secrets-manager", "vault-kv").default("env"),
+ SECRETS_REFRESH_INTERVAL_MS: Joi.number().integer().min(5000).default(60000),
+ SECRETS_EXTRA: Joi.string().allow("").default(""),
+
+ // AWS Secrets Manager
+ AWS_SECRETS_MANAGER_PREFIX: Joi.string().allow("").default(""),
+ AWS_SECRETS_MANAGER_POLL_INTERVAL_MS: Joi.number().integer().min(5000).default(60000),
+
+ // Vault KV
+ VAULT_KV_MOUNT: Joi.string().default("secret"),
+ VAULT_KV_PREFIX: Joi.string().default("vortex/"),
+ VAULT_KV_POLL_INTERVAL_MS: Joi.number().integer().min(5000).default(60000),
+
+ // Extra secret env vars referenced by the default SecretConfig
+ JWT_SIGNING_KEY: Joi.string().allow("").default(""),
+ WEBHOOK_SECRET: Joi.string().allow("").default(""),
+ CHANNEL_KEY: Joi.string().allow("").default(""),
+ // ── Egress / SSRF Protection (issue #468) ─────────────────────────────────
+ // Controls the centralized HttpEgressService used for all outbound HTTP requests
+ // (RPC, Horizon, oracles, webhooks) to prevent SSRF attacks.
+ EGRESS_TIMEOUT_MS: Joi.number().integer().min(1000).max(60000).default(10000),
+ EGRESS_MAX_REDIRECTS: Joi.number().integer().min(0).max(5).default(3),
+ EGRESS_MAX_BODY_SIZE_BYTES: Joi.number().integer().min(1024).default(10485760), // 10MB
+ SOROBAN_RPC_ALLOWLIST: Joi.string().allow("").default(""),
+ WEBHOOK_ALLOWLIST: Joi.string().allow("").default(""),
+ ORACLE_ALLOWLIST: Joi.string().allow("").default(""),
+
+ // ── WS gateway hardening (issue #455) ─────────────────────────────────────
+ WS_MAX_PAYLOAD_BYTES: Joi.number().integer().min(1024).default(16384),
+ WS_MAX_CONNECTIONS_PER_IP: Joi.number().integer().min(0).default(20),
+ // Hops of trusted reverse proxies in front of the service. 0 ignores
+ // X-Forwarded-For entirely so clients cannot spoof their IP.
+ WS_TRUST_PROXY_HOPS: Joi.number().integer().min(0).default(0),
+ WS_RATE_LIMIT_PER_SEC: Joi.number().positive().default(10),
+ WS_RATE_LIMIT_BURST: Joi.number().integer().min(1).default(20),
+ WS_RATE_LIMIT_MAX_VIOLATIONS: Joi.number().integer().min(1).default(5),
+ WS_OUTBOUND_QUEUE_MAX: Joi.number().integer().min(1).default(1000),
+ WS_OUTBOUND_BUFFER_BYTES: Joi.number().integer().min(1024).default(1048576),
+ WS_SLOW_CONSUMER_POLICY: Joi.string().valid("drop_oldest", "disconnect").default("drop_oldest"),
+ // HS256 secret shared with the SEP-10 auth endpoint (#442). Empty disables
+ // JWT auth; signature auth keeps working.
+ AUTH_JWT_SECRET: Joi.string().allow("").min(32).default(""),
+
+ // ── Distributed rate limiting (issue #441) ─────────────────────────────────
+ // How often the local rate-limiter fallback prunes expired window entries.
+ // Only relevant during a Redis outage; keeps the fallback map bounded.
+ RATE_LIMIT_LOCAL_PRUNE_MS: Joi.number().integer().min(1000).max(60000).default(60000),
+
+ // Redis URL for the distributed rate limiter (#441). Leave empty to force the
+ // bounded local limiter (the limit is still enforced, per process).
+ RATE_LIMIT_REDIS_URL: Joi.string().allow("").optional(),
+
+ // ── Scoped solver credentials (issue #443) ─────────────────────────────────
+ // Cross-replica transport for credential revocation invalidation.
+ CREDENTIAL_REVOCATION_PUBSUB: Joi.string().valid("memory", "redis").default("memory"),
+
+ // ── Solver liveness heartbeats (issue #445) ────────────────────────────────
+ // Cadence clients heartbeat at (ms) and misses tolerated before a solver
+ // is auto-marked offline (window = interval × misses).
+ SOLVER_HEARTBEAT_INTERVAL_MS: Joi.number().integer().min(1000).max(600000).default(10000),
+ SOLVER_HEARTBEAT_MISSES: Joi.number().integer().min(1).max(60).default(3),
+ // Shared liveness store URL. Defaults to REDIS_URL; leave both empty for a
+ // process-local store (single replica only — multi-replica deployments
+ // must share, otherwise each replica sees only its own heartbeats).
+ SOLVER_HEARTBEAT_REDIS_URL: Joi.string().allow("").optional(),
+
+ // ── SSE intent feed (issue #433) ───────────────────────────────────────────
+ // Heartbeat comment interval and per-client backpressure limit for the
+ // Server-Sent Events intent stream.
+ SSE_HEARTBEAT_MS: Joi.number().integer().min(1000).max(60000).default(15000),
+ SSE_MAX_BUFFER_BYTES: Joi.number().integer().min(1024).default(1048576),
+
+ // ── Health probes (issue #492) ────────────────────────────────────────────
+ // Comma-separated roles this process serves: api, ws, worker.
+ SERVICE_ROLES: Joi.string()
+ .pattern(/^(api|ws|worker)(,(api|ws|worker))*$/)
+ .default("api,ws,worker"),
+ HEALTH_CHECK_INTERVAL_MS: Joi.number().integer().min(500).default(5000),
+ HEALTH_READY_FAILURE_THRESHOLD: Joi.number().integer().min(1).default(3),
+ HEALTH_READY_SUCCESS_THRESHOLD: Joi.number().integer().min(1).default(2),
+ HEALTH_EVENT_LOOP_MAX_LAG_MS: Joi.number().integer().min(50).default(1000),
+ // Comma-separated Soroban RPC URLs for the RPC-quorum readiness check.
+ // Defaults to SOROBAN_RPC_URL.
+ SOROBAN_RPC_HEALTH_URLS: Joi.string().allow("").default(""),
+
+ // ── Soroban RPC pool & archival (#509 #510) ───────────────────────────────
+ // Comma-separated Soroban RPC endpoints; falls back to SOROBAN_RPC_URL.
+ SOROBAN_RPC_URLS: Joi.string().allow("").default(""),
+ // Optional archival RPC used for ledger-entry history reads.
+ ARCHIVAL_RPC_URL: Joi.string().uri({ scheme: ["http", "https"] }).allow("").default(""),
+ // Signing channel pool sizing for concurrent on-chain submissions.
+ CHANNEL_POOL_SIZE: Joi.number().integer().min(1).max(64).default(8),
+ // Comma-separated secret seeds, one per signing channel.
+ CHANNEL_SECRET_KEYS: Joi.string().allow("").default(""),
+ // Fee ceiling for on-chain submissions, in stroops.
+ SOROBAN_MAX_FEE_STROOPS: Joi.number().integer().min(0).default(1000000),
+ // Seconds after which a stale reconcile cursor triggers a rescan.
+ RECONCILE_STALE_SECONDS: Joi.number().integer().min(1).default(300),
+
+ // ── Postgres intents store (#404 #405) ────────────────────────────────────
+ // Backend for persisted intents: in-memory or Postgres-backed.
+ INTENTS_STORE: Joi.string().valid("memory", "postgres").default("memory"),
+ // Interval between background verification passes over the stored intents.
+ INTENTS_VERIFY_INTERVAL_MS: Joi.number().integer().min(1000).default(60000),
+
+ // ── Oracle fair-value validation (#548) ───────────────────────────────────
+ // Max quote slippage versus oracle fair value before a quote is rejected.
+ MAX_USER_SLIPPAGE_BPS: Joi.number().integer().min(0).default(100),
+ // Max premium over fair value a solver may quote before rejection.
+ MAX_PREMIUM_BPS: Joi.number().integer().min(0).default(50),
+ // Oracle staleness tolerance before prices are considered unusable.
+ ORACLE_MAX_STALENESS_MS: Joi.number().integer().min(1000).default(60000),
+ // Aggregate USD value for which a stale feed fails open instead of closed.
+ ORACLE_FAIL_OPEN_MAX_USD: Joi.number().min(0).default(100),
+
+ // ── Transactional outbox (#396 #397) ──────────────────────────────────────
+ OUTBOX_RELAY_ENABLED: Joi.boolean().default(true),
+ OUTBOX_RELAY_INTERVAL_MS: Joi.number().integer().min(100).default(2000),
+ OUTBOX_RELAY_BATCH_SIZE: Joi.number().integer().min(1).default(10),
+ OUTBOX_MAX_ATTEMPTS: Joi.number().integer().min(1).default(8),
+ OUTBOX_LEASE_SECONDS: Joi.number().integer().min(1).default(120),
+
+ // ── Durable slashing saga (#396 #397) ─────────────────────────────────────
+ SLASH_CHALLENGE_WINDOW_SECONDS: Joi.number().integer().min(0).default(600),
+ SLASH_CLOCK_SKEW_TOLERANCE_SECONDS: Joi.number().integer().min(0).default(30),
+ SLASH_MAX_SUBMIT_ATTEMPTS: Joi.number().integer().min(1).default(5),
+
+ // ── Protocol fees (#551) ──────────────────────────────────────────────────
+ // JSON array of versioned fee rules applied when quoting swaps.
+ FEE_RULES_JSON: Joi.string().default("[]"),
+ // JSON array of referral-configured fee overrides.
+ FEE_REFERRALS_JSON: Joi.string().default("[]"),
+
+ // ── EVM deposit verification (#402-#405) ──────────────────────────────────
+ EVM_DEPOSIT_VERIFICATION_ENABLED: Joi.boolean().default(false),
+ // JSON object mapping chain id to RPC URL (e.g. {"1":"https://..."}).
+ EVM_RPC_URLS: Joi.string().default("{}"),
+ // Comma-separated escrow contract addresses accepted for deposits.
+ EVM_ESCROW_ADDRESSES: Joi.string().allow("").default(""),
+ EVM_TRANSFER_FEE_TOLERANCE_BPS: Joi.number().integer().min(0).default(0),
+ EVM_LOG_LOOKBACK_BLOCKS: Joi.number().integer().min(0).default(10000),
+
+ // ── Maintenance sweeps & WS shutdown (#547 #511) ──────────────────────────
+ // Interval for the background safety sweep over stuck intents.
+ SAFETY_SWEEP_INTERVAL_MS: Joi.number().integer().min(1000).default(300000),
+ // Grace period for draining open WebSocket connections on shutdown.
+ WS_DRAIN_TIMEOUT_MS: Joi.number().integer().min(0).default(25000),
+
+ // ── Token persistence & price feed (issues #565 #566) ─────────────────────
+ // Repository backend for the token catalogue: "memory" (default, tests and
+ // single-process dev) or "prisma" (postgres-backed, restart-surviving).
+ TOKENS_PERSISTENCE: Joi.string().valid("memory", "prisma").default("memory"),
+ // JSON object mapping token symbols to CoinGecko IDs, e.g. {"BTC":"bitcoin"}.
+ PRICE_FEED_COIN_IDS: Joi.string().default("{}"),
+ // Optional CoinGecko Pro key; empty falls back to the free-tier rate limits.
+ PRICE_FEED_API_KEY: Joi.string().allow("").default(""),
+ PRICE_FEED_REFRESH_INTERVAL_MS: Joi.number().integer().min(10_000).default(60_000),
+ // Single-tick move (%) above which the circuit breaker pauses refreshes.
+ PRICE_FEED_CIRCUIT_BREAKER_THRESHOLD_PERCENT: Joi.number().min(1).max(100).default(50),
+
+ // ── RFQ quote auction (issue #570) ────────────────────────────────────────
+ // Milliseconds solvers get to respond to a quote request before the
+ // auction window closes.
+ QUOTE_AUCTION_WINDOW_MS: Joi.number().integer().min(50).default(300),
+
+ // ── Solver reputation (issue #444) ────────────────────────────────────────
+ // Weights for each reputation sub-component. Must sum to 1 in production
+ // (fail closed if misconfigured); in dev/test the app renormalises and
+ // logs a warning so local experimentation doesn't prevent boot.
+ REP_WEIGHT_FILL_RATE: Joi.number().min(0).max(1).default(0.35),
+ REP_WEIGHT_LATENCY: Joi.number().min(0).max(1).default(0.15),
+ REP_WEIGHT_SLASHES: Joi.number().min(0).max(1).default(0.25),
+ REP_WEIGHT_QUOTE_HONOUR: Joi.number().min(0).max(1).default(0.15),
+ REP_WEIGHT_VOLUME: Joi.number().min(0).max(1).default(0.10),
+ // Shared exponential-decay half-life for all event weights.
+ // Defaults to 30 days so scores are dominated by the last ~1 month.
+ REP_DECAY_HALFLIFE_SECONDS: Joi.number()
+ .integer()
+ .min(86400)
+ .default(30 * 24 * 60 * 60),
+ // Beta-distribution priors for the fill-rate Bayesian cold-start prior.
+ // (α=4, β=1) gives a ~80% prior mean so new solvers rank above poor
+ // established performers rather than at the very bottom.
+ REP_BAYES_ALPHA: Joi.number().min(0.5).default(4),
+ REP_BAYES_BETA: Joi.number().min(0.5).default(1),
+ // Volume-component knee scale, in USD-equivalent notional.
+ // $100k default: moving from $10k → $100k of decayed volume accounts
+ // for ~0.5 of the normalised volume score.
+ REP_VOLUME_LAMBDA_USD: Joi.number().min(1).default(100000),
+ // How many trailing days of daily snapshots the /reputation endpoint
+ // returns. 1..365; default 30.
+ REP_HISTORY_WINDOW_DAYS: Joi.number().integer().min(1).max(365).default(30),
+}).custom((value, helpers) => {
+ const wSum =
+ (value.REP_WEIGHT_FILL_RATE ?? 0) +
+ (value.REP_WEIGHT_LATENCY ?? 0) +
+ (value.REP_WEIGHT_SLASHES ?? 0) +
+ (value.REP_WEIGHT_QUOTE_HONOUR ?? 0) +
+ (value.REP_WEIGHT_VOLUME ?? 0);
+ // Require exact-within-tolerance only in production. Dev/test accept any
+ // weights and let configuration.ts renormalise them.
+ if (value.NODE_ENV === "production" && Math.abs(wSum - 1) > 1e-9) {
+ return helpers.message({
+ custom:
+ `Reputation weights must sum to 1.0 in production, got ${wSum.toFixed(6)} ` +
+ "from REP_WEIGHT_{FILL_RATE,LATENCY,SLASHES,QUOTE_HONOUR,VOLUME}.",
+ });
+ }
+ return value;
});
diff --git a/src/generated/api-types.ts b/src/generated/api-types.ts
index 124cdfca..43c18a41 100644
--- a/src/generated/api-types.ts
+++ b/src/generated/api-types.ts
@@ -12,14 +12,14 @@
// prettier-ignore
export interface paths {
- "/health": {
+ "/metrics": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["HealthController_check"];
+ get: operations["MetricsController_index"];
put?: never;
post?: never;
delete?: never;
@@ -28,14 +28,15 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/chain/health": {
+ "/ops/killswitch": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["SorobanController_getHealth"];
+ /** Current kill-switch state and propagation health. */
+ get: operations["KillSwitchController_status"];
put?: never;
post?: never;
delete?: never;
@@ -44,62 +45,64 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/chain/ledger": {
+ "/ops/killswitch/pause": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["SorobanController_getLatestLedger"];
+ get?: never;
put?: never;
- post?: never;
+ /** Pause a scope. Effective on every replica within the propagation budget. */
+ post: operations["KillSwitchController_pause"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/chain/network": {
+ "/ops/killswitch/resume/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["SorobanController_getNetwork"];
+ get?: never;
put?: never;
- post?: never;
+ /** Approve a resume. Takes effect once two distinct operators have approved. */
+ post: operations["KillSwitchController_approveResume"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/chain/account/{publicKey}": {
+ "/intents": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["SorobanController_getAccount"];
+ get: operations["IntentsController_list"];
put?: never;
- post?: never;
+ post: operations["IntentsController_create"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/tokens": {
+ "/intents/open": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["TokensController_getTokens"];
+ get: operations["IntentsController_listOpen"];
put?: never;
post?: never;
delete?: never;
@@ -108,14 +111,14 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/tokens/stellar": {
+ "/intents/user/{address}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["TokensController_getStellarTokens"];
+ get: operations["IntentsController_listByUser"];
put?: never;
post?: never;
delete?: never;
@@ -124,30 +127,34 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/intents": {
+ "/intents/{id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_list"];
+ get: operations["IntentsController_getOne"];
put?: never;
- post: operations["IntentsController_create"];
+ post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/open": {
+ "/intents/{id}/audit": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_listOpen"];
+ /**
+ * Get audit trail for an intent
+ * @description Returns the full state-transition history for an intent ordered oldest-first. Each entry records the state the intent moved into, who triggered it, and why.
+ */
+ get: operations["IntentsController_getAudit"];
put?: never;
post?: never;
delete?: never;
@@ -156,14 +163,14 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/intents/user/{address}": {
+ "/intents/{id}/quote": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_listByUser"];
+ get: operations["IntentsController_getPersistedQuote"];
put?: never;
post?: never;
delete?: never;
@@ -172,75 +179,79 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}": {
+ "/intents/batch": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_getOne"];
+ get?: never;
put?: never;
- post?: never;
+ /**
+ * Batch-fetch current intent records by ID
+ * @description Returns the current record for each supplied intent ID. IDs with no matching record are omitted (not individually 404'd). Capped at 100 IDs.
+ */
+ post: operations["IntentsController_batchLookup"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/audit": {
+ "/intents/batch-create": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
+ get?: never;
+ put?: never;
/**
- * Get audit trail for an intent
- * @description Returns the full state-transition history for an intent ordered oldest-first. Each entry records the state the intent moved into, who triggered it, and why.
+ * Create multiple intents atomically
+ * @description Creates up to 50 intents in a single atomic (all-or-nothing) request. If any item fails validation or exceeds open intent limits, zero intents are created and per-item errors are reported.
*/
- get: operations["IntentsController_getAudit"];
- put?: never;
- post?: never;
+ post: operations["IntentsController_batchCreate"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/quote": {
+ "/intents/{id}/accept": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_getPersistedQuote"];
+ get?: never;
put?: never;
- post?: never;
+ post: operations["IntentsController_accept"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/auction": {
+ "/intents/{id}/fill": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["IntentsController_getAuctionPrice"];
+ get?: never;
put?: never;
- post?: never;
+ post: operations["IntentsController_fill"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/accept": {
+ "/intents/{id}/cancel": {
parameters: {
query?: never;
header?: never;
@@ -249,14 +260,14 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations["IntentsController_accept"];
+ post: operations["IntentsController_cancel"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/fill": {
+ "/intents/{id}/amend": {
parameters: {
query?: never;
header?: never;
@@ -265,14 +276,14 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations["IntentsController_fill"];
+ post: operations["IntentsController_amend"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/{id}/cancel": {
+ "/intents/quote": {
parameters: {
query?: never;
header?: never;
@@ -281,14 +292,14 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations["IntentsController_cancel"];
+ post: operations["IntentsController_quote"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/intents/quote": {
+ "/intents/{id}/requote": {
parameters: {
query?: never;
header?: never;
@@ -297,21 +308,42 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations["IntentsController_quote"];
+ /** Re-quote an existing open intent using its stored fields */
+ post: operations["IntentsController_requote"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/api/v1/solvers": {
+ "/docs/ws": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["SolversController_getLeaderboard"];
+ /**
+ * AsyncAPI specification for the Vortex WebSocket feed
+ * @description Returns the AsyncAPI 2.6 YAML document describing the vortex.v1 subprotocol. Use this with AsyncAPI Studio or SDK generators.
+ */
+ get: operations["WsDocsController_getSpec"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SolversController_getLegacyLeaderboard"];
put?: never;
post: operations["SolversController_register"];
delete?: never;
@@ -320,7 +352,43 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/solvers/{address}": {
+ "/solvers/leaderboard": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Windowed solver leaderboard
+ * @description Returns the ranked solver list for a specific window. This endpoint is intended for recent-performance visibility and does not alter the legacy all-time leaderboard.
+ */
+ get: operations["SolversController_getLeaderboard"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/eligible-intents": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SolversController_getEligibleIntents"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}": {
parameters: {
query?: never;
header?: never;
@@ -336,7 +404,7 @@ export interface paths {
patch: operations["SolversController_updateSolver"];
trace?: never;
};
- "/api/v1/solvers/{address}/stats": {
+ "/solvers/{address}/stats": {
parameters: {
query?: never;
header?: never;
@@ -352,7 +420,55 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/solvers/{address}/deregister": {
+ "/solvers/{address}/slashes": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SolversController_getSlashHistory"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/slashes/{slashId}/dispute": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["SolversController_submitDispute"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/slashes/{slashId}/dispute/resolve": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post: operations["SolversController_resolveDispute"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/deregister": {
parameters: {
query?: never;
header?: never;
@@ -368,7 +484,7 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/solvers/{address}/deactivate": {
+ "/solvers/{address}/deactivate": {
parameters: {
query?: never;
header?: never;
@@ -384,7 +500,7 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/solvers/{address}/reactivate": {
+ "/solvers/{address}/reactivate": {
parameters: {
query?: never;
header?: never;
@@ -400,14 +516,15 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/stats": {
+ "/api/v1/admin/griefing": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["StatsController_getStats"];
+ /** List solvers currently under anti-griefing enforcement */
+ get: operations["SolverGriefingController_listEnforced"];
put?: never;
post?: never;
delete?: never;
@@ -416,99 +533,658 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/api/v1/stats/ws": {
+ "/api/v1/admin/griefing/{address}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get: operations["StatsController_getWsStats"];
+ /** Get anti-griefing record for a solver */
+ get: operations["SolverGriefingController_getSolverRecord"];
put?: never;
post?: never;
- delete?: never;
+ /** Reset a solver's anti-griefing state to ok */
+ delete: operations["SolverGriefingController_resetSolver"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
-}
-export type webhooks = Record;
-export interface components {
- schemas: {
- StellarTokenDto: {
- /** @description Stellar contract ID of the token */
- contract: string;
- /** @example XLM */
- symbol: string;
- /** @example Stellar Lumens */
- name: string;
- /** @example 7 */
- decimals: number;
- /** @example 0.1182 */
- priceUSD: number;
+ "/api/v1/admin/griefing/{address}/audit": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- StellarTokensResponseDto: {
- tokens: components["schemas"]["StellarTokenDto"][];
+ /** Get anti-griefing audit log for a solver */
+ get: operations["SolverGriefingController_getSolverAudit"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/admin/griefing/{address}/exclude/{intentId}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- CreateIntentDto: {
- /** @description Stellar or EVM address of the user creating the intent */
- user: string;
- /**
- * @description Source chain the funds are coming from
- * @enum {string}
- */
- srcChain: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
- /** @description Source token contract/address on srcChain */
- srcTokenAddress: string;
- /** @description Source token symbol, e.g. USDC */
- srcTokenSymbol: string;
- /** @description Source token decimals */
- srcTokenDecimals: number;
- /** @description Source amount as a non-negative integer string (base units) */
- srcAmount: string;
- /** @description Destination Stellar token contract */
- dstTokenContract: string;
- /** @description Destination token symbol, e.g. USDC */
- dstTokenSymbol: string;
- /** @description Destination token decimals */
- dstTokenDecimals: number;
- /** @description Minimum acceptable destination amount as an integer string */
- minDstAmount: string;
- /** @description Optional Dutch-auction allocation terms */
- auction?: components["schemas"]["DutchAuctionDto"];
- /** @description Unix timestamp deadline; defaults to now + 1800s; must be between now+60s and now+24h */
- deadline?: number;
- /** @description EIP-712 signature for EVM-originated intent creation */
- signature?: string;
- /** @description Unique nonce included in the EIP-712 signature */
- nonce?: string;
- /** @description Unix timestamp when the EIP-712 signature expires */
- expiresAt?: number;
- /** @description Idempotency key for deduplicating duplicate requests */
- idempotencyKey?: string;
+ get?: never;
+ put?: never;
+ /** Exclude an incident intent from griefing ratio */
+ post: operations["SolverGriefingController_excludeIncident"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/tokens": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- DutchAuctionDto: {
- /** @description Starting destination amount in base units */
- startDstAmount: string;
- /** @description Unix timestamp when the price decay starts */
- decayStart: number;
- /** @description Unix timestamp when the price reaches minDstAmount */
- decayEnd: number;
- /** @description Solver address exclusively eligible until exclusivityEnd */
- exclusiveSolver?: string;
- /** @description Unix timestamp when solver exclusivity ends */
- exclusivityEnd?: number;
+ get: operations["TokensController_getTokens"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/tokens/stellar": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
};
- AcceptIntentDto: {
- /** @description Solver address accepting the intent */
- solver: string;
- /** @description Single-use signing nonce */
- nonce?: string;
- /** @description Unix timestamp when the signature expires */
- expiresAt?: number;
- /** @description Base64-encoded Ed25519 signature of the signed intent action */
- signature: string;
+ get: operations["TokensController_getStellarTokens"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/chain/health": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SorobanController_getHealth"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/chain/ledger": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SorobanController_getLatestLedger"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/chain/network": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SorobanController_getNetwork"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/chain/account/{publicKey}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["SorobanController_getAccount"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/shadow-report": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Daily shadow-mode divergence summary
+ * @description Returns (expected, simulated) comparison counts for on-chain simulations run in parallel with the off-chain intent path, broken down by transition and by divergence reason, plus a per-UTC-day series and shadow queue health. Headline totals are lifetime-to-date; `days` bounds only the per-day breakdown.
+ */
+ get: operations["ShadowController_shadowReport"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/params": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Current and pending governance-controlled protocol parameters
+ * @description Returns the actively-enforced protocol parameters, any pending governance change with its timelock execution ledger and ETA, and recent parameter history. Parameters are sourced from the on-chain governance contract when PARAMS_CONTRACT_ID is configured; code/env defaults are used otherwise.
+ */
+ get: operations["ParamsController_getParams"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/governance/guardian/status": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Active guardian actions (with tx references) and effective pause state */
+ get: operations["GuardianController_status"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/governance/guardian/actions/{id}/override": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Superadmin override of an active guardian action (audited) */
+ post: operations["GuardianController_override"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/credentials": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List a solver's credentials
+ * @description Lists credential metadata (never plaintext) for the authenticated solver.
+ */
+ get: operations["SolverCredentialController_list"];
+ put?: never;
+ /**
+ * Create a scoped solver credential
+ * @description Mints a new scoped credential for the authenticated solver. The plaintext secret is returned ONCE. Requires a valid SEP-10 JWT for the solver.
+ */
+ post: operations["SolverCredentialController_create"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/credentials/{id}/rotate": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Rotate a solver credential
+ * @description Revokes the credential and issues a replacement with the same scopes. The new plaintext is returned ONCE.
+ */
+ post: operations["SolverCredentialController_rotate"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/solvers/{address}/credentials/{id}/revoke": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Revoke a solver credential
+ * @description Instantly revokes the credential and propagates to all replicas.
+ */
+ post: operations["SolverCredentialController_revoke"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/jobs/queues": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Depth and dead-letter counts per queue */
+ get: operations["JobsController_queues"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/jobs/queues/{queue}/dead-letters": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Most recent dead-lettered jobs for a queue */
+ get: operations["JobsController_deadLetters"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/flags": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Managed flags with env default, override pin and stored rules */
+ get: operations["FlagsController_list"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/flags/{key}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /**
+ * Set a flag's default and targeting rules
+ * @description Returns 202-style { status: 'pending' } when a second approval is required.
+ */
+ put: operations["FlagsController_update"];
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/flags/change-requests/{id}/approve": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Approve a pending change (must be a different admin than the proposer) */
+ post: operations["FlagsController_approve"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/admin/flags/{key}/audit": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Audit trail for a flag, newest first */
+ get: operations["FlagsController_auditLog"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/health/live": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["HealthController_live"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/health/ready": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["HealthController_ready"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/health/startup": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["HealthController_startup"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/health": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["HealthController_check"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/stats": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["StatsController_getStats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/stats/treasury": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["StatsController_getTreasuryStats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/stats/ws": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["StatsController_getWsStats"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/treasury/reconciliation": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get treasury reconciliation summary
+ * @description Returns daily reconciliation summary showing expected vs actual balances per asset
+ */
+ get: operations["TreasuryController_getReconciliation"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/treasury/reconciliation/{asset}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get detailed reconciliation for an asset
+ * @description Returns detailed reconciliation data including transaction breakdown
+ */
+ get: operations["TreasuryController_getAssetReconciliation"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/treasury/reconciliation/trigger": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Manually trigger treasury reconciliation
+ * @description Admin endpoint to trigger reconciliation on-demand
+ */
+ post: operations["TreasuryController_triggerReconciliation"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+export type webhooks = Record;
+export interface components {
+ schemas: {
+ PauseKillSwitchDto: {
+ /**
+ * @description Breadth of the pause.
+ * @enum {string}
+ */
+ scope: "global" | "chain" | "token" | "operation";
+ /** @description Required for chain/token/operation scope. */
+ chain?: string;
+ /** @description Required for token scope; wildcard for operation scope. */
+ token?: string;
+ /**
+ * @description Required for operation scope.
+ * @enum {string}
+ */
+ operation?: "create" | "accept" | "fill" | "slash" | "onchain";
+ /** @enum {string} */
+ reasonCode: "INCIDENT" | "TOKEN_DEPEGGED" | "SOLVER_INCIDENT" | "CHAIN_DEGRADED" | "RPC_DEGRADED" | "REGULATORY" | "MAINTENANCE";
+ /** @description Operator explanation, shown to clients and in the audit log. */
+ reason: string;
+ };
+ ApproveResumeDto: {
+ /** @description Why the resume is safe. */
+ note?: string;
+ /**
+ * @description Distinct approvals needed.
+ * @default 2
+ */
+ approvalsRequired: number;
+ };
+ CreateIntentDto: {
+ /** @description Stellar address of the user creating the intent */
+ user: string;
+ /**
+ * @description Source chain the funds are coming from
+ * @enum {string}
+ */
+ srcChain: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Source token contract/address on srcChain */
+ srcTokenAddress: string;
+ /** @description Source token symbol, e.g. USDC */
+ srcTokenSymbol: string;
+ /** @description Source token decimals */
+ srcTokenDecimals: number;
+ /** @description Source amount as a non-negative integer string (base units) */
+ srcAmount: string;
+ /** @description Destination Stellar token contract */
+ dstTokenContract: string;
+ /** @description Destination token symbol, e.g. USDC */
+ dstTokenSymbol: string;
+ /** @description Destination token decimals */
+ dstTokenDecimals: number;
+ /** @description Minimum acceptable destination amount as an integer string */
+ minDstAmount: string;
+ /** @description Unix timestamp deadline; defaults to now + 1800s; must be between now+60s and now+24h */
+ deadline?: number;
+ /** @description Idempotency key for deduplicating duplicate requests */
+ idempotencyKey?: string;
+ };
+ BatchLookupDto: {
+ /** @description Intent IDs to look up (max 100). IDs with no matching record are omitted from the response, not individually 404'd. */
+ intentIds: string[];
+ };
+ BatchCreateIntentsDto: {
+ /** @description List of intents to create atomically (1..50) */
+ intents: components["schemas"]["CreateIntentDto"][];
+ };
+ BatchCreateItemErrorDto: {
+ /** @description Zero-based index of the failed intent item in the input array */
+ index: number;
+ /** @description Field name associated with the error, if applicable */
+ field?: string;
+ /** @description Human-readable error description */
+ message: string;
+ };
+ BatchCreateIntentsResponseDto: {
+ /** @description Array of created Intent objects when successful */
+ created: string[];
+ /** @description List of per-item validation errors if any failed */
+ errors: components["schemas"]["BatchCreateItemErrorDto"][];
+ };
+ AcceptIntentDto: {
+ /** @description Solver address accepting the intent */
+ solver: string;
+ /** @description Base64-encoded Ed25519 signature of the message "accept::" produced by the solver's private key */
+ signature: string;
};
FillIntentDto: {
/** @description Solver address filling the intent (must match the accepting solver) */
@@ -517,155 +1193,1117 @@ export interface components {
fillAmount: string;
/** @description Stellar fill transaction hash */
txHash?: string;
- /** @description Single-use signing nonce */
- nonce?: string;
- /** @description Unix timestamp when the signature expires */
- expiresAt?: number;
- /** @description Base64-encoded Ed25519 signature of the signed intent action */
+ /** @description Base64-encoded Ed25519 signature of the message "fill::" produced by the solver's private key */
+ signature: string;
+ };
+ CancelIntentDto: {
+ /** @description Stellar address of the intent's original creator (must match) */
+ user: string;
+ /** @description Base64-encoded Ed25519 signature of the message "cancel:" produced by the private key of `user` */
+ signature: string;
+ };
+ AmendIntentDto: {
+ /** @description Stellar address of the intent's original creator (must match) */
+ user: string;
+ /** @description Replacement minimum destination amount in base units */
+ minDstAmount: string;
+ /** @description Replacement Unix timestamp deadline */
+ deadline: number;
+ /** @description Base64 Ed25519 signature of "amend::::" */
+ signature: string;
+ };
+ QuoteRequestDto: {
+ /**
+ * @description Source chain the funds are coming from
+ * @enum {string}
+ */
+ srcChain: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Source token symbol, e.g. USDC */
+ srcTokenSymbol: string;
+ /** @description Source amount as a non-negative integer string */
+ srcAmount: string;
+ /** @description Destination token symbol, e.g. USDC */
+ dstTokenSymbol: string;
+ /** @description Intent ID to persist the quote to */
+ intentId?: string;
+ /** @description Source token contract address / ID (used for precise token resolution) */
+ srcTokenAddress?: string;
+ /** @description Destination Stellar token contract ID (used for precise token resolution) */
+ dstTokenContract?: string;
+ };
+ RouteStepDto: {
+ /** @enum {string} */
+ type: "bridge" | "swap" | "transfer";
+ /** @description Protocol name, e.g. 'direct-solver', 'uniswap-v3' */
+ protocol: string;
+ fromChain: string;
+ toChain: string;
+ /** @description Source token info for this hop */
+ fromToken: Record;
+ /** @description Destination token info for this hop */
+ toToken: Record;
+ /** @description Estimated execution time in seconds for this step */
+ estimatedTime: number;
+ /** @description Estimated gas cost in the source token's base unit */
+ estimatedGas: string;
+ };
+ RouteDto: {
+ /** @description Ordered list of steps to execute the swap */
+ steps: components["schemas"]["RouteStepDto"][];
+ /** @description Total estimated time for all steps in seconds */
+ totalTime: number;
+ /** @description Total fees in USD across all steps */
+ totalFeesUSD: number;
+ /** @description Estimated price impact as a decimal fraction, e.g. 0.003 = 0.3% */
+ priceImpact: number;
+ };
+ QuoteDto: {
+ /** @description Solver address */
+ solver: string;
+ /** @description Solver name */
+ solverName: string;
+ /** @description Destination amount as a string */
+ dstAmount: string;
+ /** @description Protocol fee as a string */
+ fee: string;
+ /** @description Estimated fill time in seconds */
+ fillTime: number;
+ /** @description Unix timestamp when quote expires */
+ expiresAt: number;
+ /** @description Total fees in USD (protocol fee converted at token price) */
+ totalFeesUSD: number;
+ /** @description Estimated price impact as a decimal fraction, e.g. 0.003 = 0.3% */
+ priceImpact: number;
+ /** @description Computed execution route (direct single-step or multi-hop via USDC intermediate) */
+ route: components["schemas"]["RouteDto"];
+ };
+ QuoteResponseDto: {
+ /** @description Array of quotes sorted by best dstAmount first */
+ quotes: components["schemas"]["QuoteDto"][];
+ /** @description Best quote or null if no solvers available */
+ bestQuote: components["schemas"]["QuoteDto"] | null;
+ /** @description Source chain */
+ srcChain: string;
+ /** @description Source token symbol */
+ srcTokenSymbol: string;
+ /** @description Source amount as a string */
+ srcAmount: string;
+ /** @description Destination token symbol */
+ dstTokenSymbol: string;
+ /** @description Estimated fill time in seconds for the best quote */
+ estimatedFillTime: number;
+ /** @description Total fees in USD for the best quote (0 when no quote available) */
+ totalFeesUSD: number;
+ /** @description Price impact for the best quote as a decimal fraction (0 when no quote available) */
+ priceImpact: number;
+ /** @description True when no solver responded and the returned quote is indicative */
+ indicative?: boolean;
+ };
+ RegisterSolverDto: {
+ /** @description Solver's Stellar address */
+ address: string;
+ /** @description Solver's display name */
+ name: string;
+ /** @description Bond amount as a non-negative integer string (in USDC base units) */
+ bondAmount: string;
+ /** @description Average fill time in seconds */
+ avgFillTime: number;
+ /** @description Chains this solver supports */
+ supportedChains: ("stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche")[];
+ /** @description Token symbols this solver supports */
+ supportedTokens: unknown[][];
+ /** @description Proof-of-control signature for the advertised solver address */
+ proofSignature: string;
+ };
+ UpdateSolverDto: {
+ /** @description New display name */
+ name?: string;
+ /** @description Replacement list of chains this solver supports */
+ supportedChains?: ("stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche")[];
+ /** @description Replacement list of supported token symbols */
+ supportedTokens?: unknown[][];
+ /** @description Updated average fill time in seconds */
+ avgFillTime?: number;
+ /** @description Base64-encoded Ed25519 signature of the message "update-solver:" produced by the solver's private key, proving control of :address */
signature: string;
};
- CancelIntentDto: {
- /** @description Stellar or EVM address of the intent's original creator (must match) */
- user: string;
- /** @description Single-use signing nonce */
- nonce?: string;
- /** @description Unix timestamp when the signature expires */
- expiresAt?: number;
- /** @description Base64 Ed25519 or EIP-712 signature proving control of user */
- signature: string;
+ UpdateSolverStatusDto: {
+ /** @description Proof-of-control signature for the solver status update */
+ signature: string;
+ };
+ StellarTokenDto: {
+ /** @description Stellar contract ID of the token */
+ contract: string;
+ /** @example XLM */
+ symbol: string;
+ /** @example Stellar Lumens */
+ name: string;
+ /** @example 7 */
+ decimals: number;
+ /** @example 0.1182 */
+ priceUSD: number;
+ };
+ StellarTokensResponseDto: {
+ tokens: components["schemas"]["StellarTokenDto"][];
+ };
+ GuardianOverrideDto: Record;
+ CreateSolverCredentialDto: {
+ /**
+ * @description Scopes granted to the credential
+ * @enum {string}
+ */
+ scopes: "solver:read" | "intents:read" | "quote:respond" | "intents:accept" | "intents:fill";
+ /** @description Optional source-address allowlist. Entries are an exact IPv4 address, an IPv4 CIDR block, an exact IPv6 address, or `*` for any source. IPv6 CIDR blocks and hostnames are not supported; an entry that does not parse never matches, so a typo locks the credential out rather than opening it up. */
+ ipAllowlist?: string[];
+ /** @description Unix epoch seconds at which the credential expires (null = never) */
+ expiresAt?: number;
+ };
+ UpdateFlagDto: Record;
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record;
+export interface operations {
+ MetricsController_index: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ KillSwitchController_status: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ KillSwitchController_pause: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["PauseKillSwitchDto"];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ KillSwitchController_approveResume: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["ApproveResumeDto"];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_list: {
+ parameters: {
+ query?: {
+ /** @description Filter by intent state */
+ state?: "open" | "accepted" | "filled" | "cancelled" | "expired" | "slashed" | "pending_open" | "pending_accepted" | "pending_filled" | "pending_cancelled";
+ /** @description Filter by user address */
+ user?: string;
+ /** @description Filter by source chain */
+ chain?: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Number of results per page (max 100) */
+ limit?: number;
+ /** @description Cursor for the next page of intents */
+ cursor?: string;
+ /** @description Number of results to skip */
+ offset?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Invalid limit or offset */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_create: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateIntentDto"];
+ };
+ };
+ responses: {
+ /** @description Invalid request body */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Open-intent cap reached — a single user may not hold more than 50 open/accepted intents simultaneously */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Rate limit exceeded — max 10 intent creations per user per 60 s (or 100 req/min per IP globally) */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_listOpen: {
+ parameters: {
+ query?: {
+ /** @description Filter by intent state */
+ state?: "open" | "accepted" | "filled" | "cancelled" | "expired" | "slashed" | "pending_open" | "pending_accepted" | "pending_filled" | "pending_cancelled";
+ /** @description Filter by user address */
+ user?: string;
+ /** @description Filter by source chain */
+ chain?: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Number of results per page (max 100) */
+ limit?: number;
+ /** @description Cursor for the next page of intents */
+ cursor?: string;
+ /** @description Number of results to skip */
+ offset?: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_listByUser: {
+ parameters: {
+ query?: {
+ /** @description Filter by intent state */
+ state?: "open" | "accepted" | "filled" | "cancelled" | "expired" | "slashed" | "pending_open" | "pending_accepted" | "pending_filled" | "pending_cancelled";
+ /** @description Filter by user address */
+ user?: string;
+ /** @description Filter by source chain */
+ chain?: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Number of results per page (max 100) */
+ limit?: number;
+ /** @description Cursor for the next page of intents */
+ cursor?: string;
+ /** @description Number of results to skip */
+ offset?: number;
+ };
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_getOne: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_getAudit: {
+ parameters: {
+ query?: {
+ /** @description Filter by intent state */
+ state?: "open" | "accepted" | "filled" | "cancelled" | "expired" | "slashed" | "pending_open" | "pending_accepted" | "pending_filled" | "pending_cancelled";
+ /** @description Filter by user address */
+ user?: string;
+ /** @description Filter by source chain */
+ chain?: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Number of results per page (max 100) */
+ limit?: number;
+ /** @description Cursor for the next page of intents */
+ cursor?: string;
+ /** @description Number of results to skip */
+ offset?: number;
+ };
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Audit trail for the intent */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ intentId?: string;
+ entries?: {
+ /** Format: date-time */
+ timestamp?: string;
+ toState?: string;
+ actor?: string;
+ reason?: string;
+ metadata?: Record | null;
+ }[];
+ };
+ };
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_getPersistedQuote: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Persisted quote for the intent */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent not found or no quote persisted */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_batchLookup: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BatchLookupDto"];
+ };
+ };
+ responses: {
+ /** @description Records for the found intent IDs, plus a count */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description intentIds missing, not an array of strings, or exceeds 100 entries */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_batchCreate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["BatchCreateIntentsDto"];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["BatchCreateIntentsResponseDto"];
+ };
+ };
+ /** @description Invalid request payload or empty batch */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Per-item validation errors or limits exceeded */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_accept: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AcceptIntentDto"];
+ };
+ };
+ responses: {
+ /** @description Solver not registered or inactive */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent is not in open state */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent has expired */
+ 410: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_fill: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["FillIntentDto"];
+ };
+ };
+ responses: {
+ /** @description Fill amount below minimum */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Wrong solver for this intent */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent is not in accepted state */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Fill window has expired */
+ 410: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description An emergency kill-switch is active for this intent's scope (503 + Retry-After) */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_cancel: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CancelIntentDto"];
+ };
+ };
+ responses: {
+ /** @description Unauthorized */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent is not in open state */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_amend: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["AmendIntentDto"];
+ };
+ };
+ responses: {
+ /** @description Unauthorized */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent is not amendable */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_quote: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["QuoteRequestDto"];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["QuoteResponseDto"];
+ };
+ };
+ /** @description Rate limit exceeded — max 20 quote requests per 60 s per IP */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ IntentsController_requote: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["QuoteResponseDto"];
+ };
+ };
+ /** @description Intent not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Intent is not in the open state */
+ 409: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Rate limit exceeded — max 20 quote requests per 60 s per IP */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ WsDocsController_getSpec: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_getLegacyLeaderboard: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_register: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["RegisterSolverDto"];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_getLeaderboard: {
+ parameters: {
+ query?: {
+ window?: "24h" | "7d" | "30d" | "all";
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_getEligibleIntents: {
+ parameters: {
+ query?: {
+ /** @description Filter by intent state */
+ state?: "open" | "accepted" | "filled" | "cancelled" | "expired" | "slashed" | "pending_open" | "pending_accepted" | "pending_filled" | "pending_cancelled";
+ /** @description Filter by user address */
+ user?: string;
+ /** @description Filter by source chain */
+ chain?: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
+ /** @description Number of results per page (max 100) */
+ limit?: number;
+ /** @description Cursor for the next page of intents */
+ cursor?: string;
+ /** @description Number of results to skip */
+ offset?: number;
+ };
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_getSolver: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_updateSolver: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateSolverDto"];
+ };
};
- QuoteRequestDto: {
- /**
- * @description Source chain the funds are coming from
- * @enum {string}
- */
- srcChain: "stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche";
- /** @description Source token symbol, e.g. USDC */
- srcTokenSymbol: string;
- /** @description Source amount as a non-negative integer string */
- srcAmount: string;
- /** @description Destination token symbol, e.g. USDC */
- dstTokenSymbol: string;
- /** @description Intent ID to persist the quote to */
- intentId?: string;
- /** @description Source token contract address / ID (used for precise token resolution) */
- srcTokenAddress?: string;
- /** @description Destination Stellar token contract ID (used for precise token resolution) */
- dstTokenContract?: string;
+ responses: {
+ /** @description Updated solver record */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Invalid update body */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Missing or invalid signature */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Solver not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- RouteStepDto: {
- /** @enum {string} */
- type: "bridge" | "swap" | "transfer";
- /** @description Protocol name, e.g. 'direct-solver', 'uniswap-v3' */
- protocol: string;
- fromChain: string;
- toChain: string;
- /** @description Source token info for this hop */
- fromToken: Record;
- /** @description Destination token info for this hop */
- toToken: Record;
- /** @description Estimated execution time in seconds for this step */
- estimatedTime: number;
- /** @description Estimated gas cost in the source token's base unit */
- estimatedGas: string;
+ };
+ SolversController_getSolverStats: {
+ parameters: {
+ query: {
+ window: string;
+ };
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
};
- RouteDto: {
- /** @description Ordered list of steps to execute the swap */
- steps: components["schemas"]["RouteStepDto"][];
- /** @description Total estimated time for all steps in seconds */
- totalTime: number;
- /** @description Total fees in USD across all steps */
- totalFeesUSD: number;
- /** @description Estimated price impact as a decimal fraction, e.g. 0.003 = 0.3% */
- priceImpact: number;
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- QuoteDto: {
- /** @description Solver address */
- solver: string;
- /** @description Solver name */
- solverName: string;
- /** @description Destination amount as a string */
- dstAmount: string;
- /** @description Protocol fee as a string */
- fee: string;
- /** @description Estimated fill time in seconds */
- fillTime: number;
- /** @description Unix timestamp when quote expires */
- expiresAt: number;
- /** @description Total fees in USD (protocol fee converted at token price) */
- totalFeesUSD: number;
- /** @description Estimated price impact as a decimal fraction, e.g. 0.003 = 0.3% */
- priceImpact: number;
- /** @description Computed execution route (direct single-step or multi-hop via USDC intermediate) */
- route: components["schemas"]["RouteDto"];
+ };
+ SolversController_getSlashHistory: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
};
- QuoteResponseDto: {
- /** @description Array of quotes sorted by best dstAmount first */
- quotes: components["schemas"]["QuoteDto"][];
- /** @description Best quote or null if no solvers available */
- bestQuote: components["schemas"]["QuoteDto"] | null;
- /** @description Source chain */
- srcChain: string;
- /** @description Source token symbol */
- srcTokenSymbol: string;
- /** @description Source amount as a string */
- srcAmount: string;
- /** @description Destination token symbol */
- dstTokenSymbol: string;
- /** @description Estimated fill time in seconds for the best quote */
- estimatedFillTime: number;
- /** @description Total fees in USD for the best quote (0 when no quote available) */
- totalFeesUSD: number;
- /** @description Price impact for the best quote as a decimal fraction (0 when no quote available) */
- priceImpact: number;
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
- RegisterSolverDto: {
- /** @description Solver's Stellar address */
- address: string;
- /** @description Solver's display name */
- name: string;
- /** @description Bond amount as a non-negative integer string (in USDC base units) */
- bondAmount: string;
- /** @description Average fill time in seconds */
- avgFillTime: number;
- /** @description Chains this solver supports */
- supportedChains: ("stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche")[];
- /** @description Token symbols this solver supports */
- supportedTokens: unknown[][];
- /** @description Proof-of-control signature for the advertised solver address */
- proofSignature: string;
+ };
+ SolversController_submitDispute: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ slashId: string;
+ };
+ cookie?: never;
};
- UpdateSolverDto: {
- /** @description New display name */
- name?: string;
- /** @description Replacement list of chains this solver supports */
- supportedChains?: ("stellar" | "ethereum" | "base" | "polygon" | "arbitrum" | "optimism" | "avalanche")[];
- /** @description Replacement list of supported token symbols */
- supportedTokens?: unknown[][];
- /** @description Updated average fill time in seconds */
- avgFillTime?: number;
- /** @description Base64-encoded Ed25519 signature of the message "update-solver:" produced by the solver's private key, proving control of :address */
- signature: string;
+ requestBody?: never;
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
};
};
- responses: never;
- parameters: never;
- requestBodies: never;
- headers: never;
- pathItems: never;
-}
-export type $defs = Record;
-export interface operations {
- HealthController_check: {
+ SolversController_resolveDispute: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ slashId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_deregisterSolver: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateSolverStatusDto"];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_deactivate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateSolverStatusDto"];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ };
+ };
+ SolversController_reactivate: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ address: string;
+ };
cookie?: never;
};
- requestBody?: never;
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["UpdateSolverStatusDto"];
+ };
+ };
responses: {
- 200: {
+ 201: {
headers: {
[name: string]: unknown;
};
@@ -673,7 +2311,7 @@ export interface operations {
};
};
};
- SorobanController_getHealth: {
+ SolverGriefingController_listEnforced: {
parameters: {
query?: never;
header?: never;
@@ -682,111 +2320,84 @@ export interface operations {
};
requestBody?: never;
responses: {
- /** @description Soroban RPC node health status (pass-through of the RPC `getHealth` result). */
200: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": {
- /** @example healthy */
- status: string;
- latestLedger?: number;
- oldestLedger?: number;
- ledgerRetentionWindow?: number;
- };
- };
+ content?: never;
};
};
};
- SorobanController_getLatestLedger: {
+ SolverGriefingController_getSolverRecord: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ address: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Latest closed ledger as reported by the Soroban RPC node. */
200: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": {
- id: string;
- /** @example 12345678 */
- sequence: number;
- protocolVersion?: number;
- };
- };
+ content?: never;
};
};
};
- SorobanController_getNetwork: {
+ SolverGriefingController_resetSolver: {
parameters: {
query?: never;
header?: never;
- path?: never;
+ path: {
+ address: string;
+ };
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Network passphrase and protocol metadata for the configured RPC node. */
200: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": {
- friendbotUrl?: string | null;
- /** @example Test SDF Network ; September 2015 */
- passphrase: string;
- protocolVersion?: number;
- };
- };
+ content?: never;
};
};
};
- SorobanController_getAccount: {
+ SolverGriefingController_getSolverAudit: {
parameters: {
query?: never;
header?: never;
path: {
- /** @description Stellar Ed25519 account public key (starts with `G`, 56 characters). */
- publicKey: string;
+ address: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description On-chain account record (id, sequence number, and balances). */
200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- "application/json": {
- id: string;
- /** @example 987654321 */
- sequence: string;
- balances?: {
- balance?: string;
- asset_type?: string;
- }[];
- };
- };
- };
- /** @description Invalid Stellar public key format */
- 400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Per-account rate limit exceeded (AccountRateLimitGuard) */
- 429: {
+ };
+ };
+ SolverGriefingController_excludeIncident: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ intentId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 201: {
headers: {
[name: string]: unknown;
};
@@ -877,67 +2488,58 @@ export interface operations {
};
};
};
- IntentsController_list: {
+ SorobanController_getHealth: {
parameters: {
- query: {
- /** @description Filter by intent state */
- state?: string;
- /** @description Filter by user address */
- user?: string;
- /** @description Filter by source chain */
- chain?: string;
- /** @description Number of results per page */
- limit: number;
- /** @description Cursor for the next page of intents */
- cursor?: string;
- /** @description Number of results to skip */
- offset: number;
- };
+ query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Invalid limit or offset */
- 400: {
+ /** @description Soroban RPC node health status (pass-through of the RPC `getHealth` result). */
+ 200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": {
+ /** @example healthy */
+ status: string;
+ latestLedger?: number;
+ oldestLedger?: number;
+ ledgerRetentionWindow?: number;
+ };
+ };
};
};
};
- IntentsController_create: {
+ SorobanController_getLatestLedger: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["CreateIntentDto"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Invalid request body */
- 400: {
+ /** @description Latest closed ledger as reported by the Soroban RPC node. */
+ 200: {
headers: {
[name: string]: unknown;
};
- content?: never;
- };
- /** @description Rate limit exceeded — max 10 intent creations per user per 60 s (or 100 req/min per IP globally) */
- 429: {
- headers: {
- [name: string]: unknown;
+ content: {
+ "application/json": {
+ id: string;
+ /** @example 12345678 */
+ sequence: number;
+ protocolVersion?: number;
+ };
};
- content?: never;
};
};
};
- IntentsController_listOpen: {
+ SorobanController_getNetwork: {
parameters: {
query?: never;
header?: never;
@@ -946,46 +2548,60 @@ export interface operations {
};
requestBody?: never;
responses: {
+ /** @description Network passphrase and protocol metadata for the configured RPC node. */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": {
+ friendbotUrl?: string | null;
+ /** @example Test SDF Network ; September 2015 */
+ passphrase: string;
+ protocolVersion?: number;
+ };
+ };
};
};
};
- IntentsController_listByUser: {
+ SorobanController_getAccount: {
parameters: {
query?: never;
header?: never;
path: {
- address: string;
+ /** @description Stellar Ed25519 account public key (starts with `G`, 56 characters). */
+ publicKey: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
+ /** @description On-chain account record (id, sequence number, and balances). */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": {
+ id: string;
+ /** @example 987654321 */
+ sequence: string;
+ balances?: {
+ balance?: string;
+ asset_type?: string;
+ }[];
+ };
+ };
};
- };
- };
- IntentsController_getOne: {
- parameters: {
- query?: never;
- header?: never;
- path: {
- id: string;
+ /** @description Invalid Stellar public key format */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
};
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description Intent not found */
- 404: {
+ /** @description Per-account rate limit exceeded (AccountRateLimitGuard) */
+ 429: {
headers: {
[name: string]: unknown;
};
@@ -993,99 +2609,161 @@ export interface operations {
};
};
};
- IntentsController_getAudit: {
+ ShadowController_shadowReport: {
parameters: {
- query?: never;
- header?: never;
- path: {
- id: string;
+ query?: {
+ /** @description Trailing UTC days of per-day breakdown to include (1-90, default 1). */
+ days?: number;
};
+ header?: never;
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Audit trail for the intent */
+ /** @description Shadow-mode divergence report */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
- intentId?: string;
- entries?: {
- /** Format: date-time */
- timestamp?: string;
- toState?: string;
- actor?: string;
- reason?: string;
- metadata?: Record | null;
+ enabled?: boolean;
+ sampleRate?: number;
+ /** @example 2026-09-30 */
+ day?: string;
+ /** Format: date-time */
+ generatedAt?: string;
+ compared?: number;
+ diverged?: number;
+ divergenceRate?: number;
+ transitions?: {
+ /** @example accept */
+ transition?: string;
+ compared?: number;
+ diverged?: number;
+ divergenceRate?: number;
+ }[];
+ divergences?: {
+ /** @example fill */
+ transition?: string;
+ /**
+ * @example outcome_mismatch
+ * @enum {string}
+ */
+ reason?: "outcome_mismatch" | "simulation_error" | "simulation_exception" | "contract_unconfigured";
+ count?: number;
}[];
+ daily?: {
+ /** @example 2026-09-30 */
+ day?: string;
+ compared?: number;
+ diverged?: number;
+ divergenceRate?: number;
+ cells?: Record[];
+ }[];
+ queue?: {
+ depth?: number;
+ capacity?: number;
+ dropped?: number;
+ sampledOut?: number;
+ disabled?: number;
+ completed?: number;
+ };
};
};
};
- /** @description Intent not found */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
};
};
- IntentsController_getPersistedQuote: {
+ ParamsController_getParams: {
parameters: {
query?: never;
header?: never;
- path: {
- id: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- /** @description Persisted quote for the intent */
+ /** @description Protocol parameters — current, pending, and history */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
- };
- /** @description Intent not found or no quote persisted */
- 404: {
- headers: {
- [name: string]: unknown;
+ content: {
+ "application/json": {
+ /** @description Currently active protocol parameters */
+ current: {
+ /** @example 3 */
+ version: number;
+ /**
+ * @description Protocol fee in basis points
+ * @example 30
+ */
+ feeBps: number;
+ /** @description Per-chain deadline and fill-window overrides */
+ chains: {
+ [key: string]: {
+ /** @example 900 */
+ deadlineSeconds?: number;
+ /** @example 120 */
+ fillWindowSeconds?: number;
+ };
+ };
+ /**
+ * @description Maximum on-chain exposure ratio (0–1)
+ * @example 0.05
+ */
+ maxExposureRatio: number;
+ /** @example 100000000 */
+ slashAmount: string;
+ /** @example 12345678 */
+ activeSinceLedger: number;
+ /** Format: date-time */
+ adoptedAt: string;
+ };
+ /** @description Scheduled governance change not yet activated, or null */
+ pending: ({
+ params?: Record;
+ /** @example 12349999 */
+ executionLedger?: number;
+ /**
+ * Format: date-time
+ * @description Best-effort ETA for timelock execution
+ */
+ estimatedEta?: string;
+ /** Format: date-time */
+ observedAt?: string;
+ } | null) | null;
+ /** @description Previous parameter versions, newest-first (max 50) */
+ history: Record[];
+ };
};
- content?: never;
};
};
};
- IntentsController_getAuctionPrice: {
+ GuardianController_status: {
parameters: {
query?: never;
header?: never;
- path: { id: string };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: {
- headers: { [name: string]: unknown };
- content: {
- "application/json": {
- intentId: string;
- currentDstAmount: string;
- acceptedDstAmount?: string;
- timestamp: number;
- };
+ headers: {
+ [name: string]: unknown;
};
+ content?: never;
};
- 404: { headers: { [name: string]: unknown }; content?: never };
};
};
- IntentsController_accept: {
+ GuardianController_override: {
parameters: {
query?: never;
- header?: never;
+ header: {
+ "x-admin-key": string;
+ };
path: {
id: string;
};
@@ -1093,33 +2771,38 @@ export interface operations {
};
requestBody: {
content: {
- "application/json": components["schemas"]["AcceptIntentDto"];
+ "application/json": components["schemas"]["GuardianOverrideDto"];
};
};
responses: {
- /** @description Solver not registered or inactive */
- 403: {
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Intent not found */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
+ };
+ };
+ SolverCredentialController_list: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
};
- /** @description Intent is not in open state */
- 409: {
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Credential metadata list */
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Intent has expired */
- 410: {
+ /** @description Missing or invalid SEP-10 JWT */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -1127,51 +2810,62 @@ export interface operations {
};
};
};
- IntentsController_fill: {
+ SolverCredentialController_create: {
parameters: {
query?: never;
header?: never;
path: {
- id: string;
+ address: string;
};
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["FillIntentDto"];
+ "application/json": components["schemas"]["CreateSolverCredentialDto"];
};
};
responses: {
- /** @description Fill amount below minimum */
- 400: {
+ /** @description Credential created (plaintext shown once) */
+ 201: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Wrong solver for this intent */
- 403: {
+ /** @description Missing or invalid SEP-10 JWT */
+ 401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Intent not found */
- 404: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
+ };
+ };
+ SolverCredentialController_rotate: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ address: string;
+ id: string;
};
- /** @description Intent is not in accepted state */
- 409: {
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CreateSolverCredentialDto"];
+ };
+ };
+ responses: {
+ /** @description Credential rotated (new plaintext shown once) */
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Fill window has expired */
- 410: {
+ /** @description Missing or invalid SEP-10 JWT */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -1179,37 +2873,27 @@ export interface operations {
};
};
};
- IntentsController_cancel: {
+ SolverCredentialController_revoke: {
parameters: {
query?: never;
header?: never;
path: {
+ address: string;
id: string;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["CancelIntentDto"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Unauthorized */
- 403: {
- headers: {
- [name: string]: unknown;
- };
- content?: never;
- };
- /** @description Intent not found */
- 404: {
+ /** @description Credential revoked */
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Intent is not in open state */
- 409: {
+ /** @description Missing or invalid SEP-10 JWT */
+ 401: {
headers: {
[name: string]: unknown;
};
@@ -1217,29 +2901,39 @@ export interface operations {
};
};
};
- IntentsController_quote: {
+ JobsController_queues: {
parameters: {
query?: never;
- header?: never;
+ header: {
+ "x-admin-key": string;
+ };
path?: never;
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["QuoteRequestDto"];
- };
- };
+ requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
- content: {
- "application/json": components["schemas"]["QuoteResponseDto"];
- };
+ content?: never;
};
- /** @description Rate limit exceeded — max 20 quote requests per 60 s per IP */
- 429: {
+ };
+ };
+ JobsController_deadLetters: {
+ parameters: {
+ query?: never;
+ header: {
+ "x-admin-key": string;
+ };
+ path: {
+ queue: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1247,10 +2941,12 @@ export interface operations {
};
};
};
- SolversController_getLeaderboard: {
+ FlagsController_list: {
parameters: {
query?: never;
- header?: never;
+ header: {
+ "x-admin-key": string;
+ };
path?: never;
cookie?: never;
};
@@ -1264,20 +2960,24 @@ export interface operations {
};
};
};
- SolversController_register: {
+ FlagsController_update: {
parameters: {
query?: never;
- header?: never;
- path?: never;
+ header: {
+ "x-admin-key": string;
+ };
+ path: {
+ key: string;
+ };
cookie?: never;
};
requestBody: {
content: {
- "application/json": components["schemas"]["RegisterSolverDto"];
+ "application/json": components["schemas"]["UpdateFlagDto"];
};
};
responses: {
- 201: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1285,12 +2985,14 @@ export interface operations {
};
};
};
- SolversController_getSolver: {
+ FlagsController_approve: {
parameters: {
query?: never;
- header?: never;
+ header: {
+ "x-admin-key": string;
+ };
path: {
- address: string;
+ id: string;
};
cookie?: never;
};
@@ -1304,44 +3006,71 @@ export interface operations {
};
};
};
- SolversController_updateSolver: {
+ FlagsController_auditLog: {
parameters: {
query?: never;
- header?: never;
+ header: {
+ "x-admin-key": string;
+ };
path: {
- address: string;
+ key: string;
};
cookie?: never;
};
- requestBody: {
- content: {
- "application/json": components["schemas"]["UpdateSolverDto"];
- };
- };
+ requestBody?: never;
responses: {
- /** @description Updated solver record */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Invalid update body */
- 400: {
+ };
+ };
+ HealthController_live: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Missing or invalid signature */
- 401: {
+ };
+ };
+ HealthController_ready: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
- /** @description Solver not found */
- 404: {
+ };
+ };
+ HealthController_startup: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1349,13 +3078,11 @@ export interface operations {
};
};
};
- SolversController_getSolverStats: {
+ HealthController_check: {
parameters: {
query?: never;
header?: never;
- path: {
- address: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
@@ -1368,18 +3095,16 @@ export interface operations {
};
};
};
- SolversController_deregisterSolver: {
+ StatsController_getStats: {
parameters: {
query?: never;
header?: never;
- path: {
- address: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- 201: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1387,18 +3112,16 @@ export interface operations {
};
};
};
- SolversController_deactivate: {
+ StatsController_getTreasuryStats: {
parameters: {
query?: never;
header?: never;
- path: {
- address: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- 201: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1406,18 +3129,16 @@ export interface operations {
};
};
};
- SolversController_reactivate: {
+ StatsController_getWsStats: {
parameters: {
query?: never;
header?: never;
- path: {
- address: string;
- };
+ path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
- 201: {
+ 200: {
headers: {
[name: string]: unknown;
};
@@ -1425,37 +3146,143 @@ export interface operations {
};
};
};
- StatsController_getStats: {
+ TreasuryController_getReconciliation: {
parameters: {
- query?: never;
+ query?: {
+ /** @description Date in YYYY-MM-DD format (defaults to today) */
+ date?: string;
+ };
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
+ /** @description Reconciliation summary */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": {
+ /** @example 2026-09-28 */
+ date?: string;
+ assets?: {
+ /** @example native */
+ asset?: string;
+ /** @example 1000000000 */
+ expectedBalance?: string;
+ /** @example 1000500000 */
+ actualBalance?: string;
+ /** @example 500000 */
+ discrepancy?: string;
+ /** @example 0.05 */
+ discrepancyPercentage?: number;
+ /** @example false */
+ hasUnexplainedDiscrepancy?: boolean;
+ explanation?: string | null;
+ }[];
+ /** @example 3 */
+ totalDiscrepancies?: number;
+ /** @example 1 */
+ assetsWithUnexplainedDiscrepancies?: number;
+ /** @example 2026-09-28T00:00:00.000Z */
+ lastReconciliationAt?: string;
+ };
+ };
};
};
};
- StatsController_getWsStats: {
+ TreasuryController_getAssetReconciliation: {
parameters: {
- query?: never;
+ query?: {
+ /** @description Date in YYYY-MM-DD format (defaults to today) */
+ date?: string;
+ };
+ header?: never;
+ path: {
+ /** @description Asset identifier (e.g., 'native', 'USDC:ISSUER', or contract address) */
+ asset: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Detailed reconciliation data */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example 2026-09-28 */
+ snapshotDate?: string;
+ /** @example native */
+ asset?: string;
+ /** @example 1000000000 */
+ expectedBalance?: string;
+ /** @example 1000500000 */
+ actualBalance?: string;
+ /** @example 500000 */
+ discrepancy?: string;
+ /** @example 0.05 */
+ discrepancyPercentage?: number;
+ /** @example 10000000 */
+ toleranceThreshold?: string;
+ /** @example false */
+ hasUnexplainedDiscrepancy?: boolean;
+ explanation?: string | null;
+ breakdown?: {
+ /** @example 500000000 */
+ fees?: string;
+ /** @example 100000000 */
+ slashes?: string;
+ /** @example 50000000 */
+ refunds?: string;
+ };
+ recentTransactions?: {
+ /** @enum {string} */
+ type?: "fee" | "slash" | "refund";
+ /** @example 1000000 */
+ amount?: string;
+ /** @example 2026-09-28T12:00:00.000Z */
+ timestamp?: string;
+ /** @example intent-uuid */
+ reference?: string;
+ }[];
+ };
+ };
+ };
+ };
+ };
+ TreasuryController_triggerReconciliation: {
+ parameters: {
+ query?: {
+ /** @description Specific asset to reconcile (reconciles all if omitted) */
+ asset?: string;
+ };
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
+ /** @description Reconciliation completed */
200: {
headers: {
[name: string]: unknown;
};
- content?: never;
+ content: {
+ "application/json": {
+ /** @example Reconciliation completed */
+ message?: string;
+ results?: {
+ asset?: string;
+ hasUnexplainedDiscrepancy?: boolean;
+ discrepancy?: string;
+ }[];
+ };
+ };
};
};
};
diff --git a/src/generated/openapi.json b/src/generated/openapi.json
index d966887b..5bf3a2bb 100644
--- a/src/generated/openapi.json
+++ b/src/generated/openapi.json
@@ -1,21 +1,1683 @@
{
"openapi": "3.0.0",
"paths": {
- "/health": {
+ "/metrics": {
+ "get": {
+ "operationId": "MetricsController_index",
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "metrics"
+ ]
+ }
+ },
+ "/ops/killswitch": {
+ "get": {
+ "operationId": "KillSwitchController_status",
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "summary": "Current kill-switch state and propagation health.",
+ "tags": [
+ "ops/killswitch"
+ ]
+ }
+ },
+ "/ops/killswitch/pause": {
+ "post": {
+ "operationId": "KillSwitchController_pause",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PauseKillSwitchDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "summary": "Pause a scope. Effective on every replica within the propagation budget.",
+ "tags": [
+ "ops/killswitch"
+ ]
+ }
+ },
+ "/ops/killswitch/resume/{id}": {
+ "post": {
+ "operationId": "KillSwitchController_approveResume",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApproveResumeDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "summary": "Approve a resume. Takes effect once two distinct operators have approved.",
+ "tags": [
+ "ops/killswitch"
+ ]
+ }
+ },
+ "/intents": {
+ "get": {
+ "operationId": "IntentsController_list",
+ "parameters": [
+ {
+ "name": "state",
+ "required": false,
+ "in": "query",
+ "description": "Filter by intent state",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "open",
+ "accepted",
+ "filled",
+ "cancelled",
+ "expired",
+ "slashed",
+ "pending_open",
+ "pending_accepted",
+ "pending_filled",
+ "pending_cancelled"
+ ]
+ }
+ },
+ {
+ "name": "user",
+ "required": false,
+ "in": "query",
+ "description": "Filter by user address",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "chain",
+ "required": false,
+ "in": "query",
+ "description": "Filter by source chain",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "stellar",
+ "ethereum",
+ "base",
+ "polygon",
+ "arbitrum",
+ "optimism",
+ "avalanche"
+ ]
+ }
+ },
+ {
+ "name": "limit",
+ "required": false,
+ "in": "query",
+ "description": "Number of results per page (max 100)",
+ "schema": {
+ "minimum": 1,
+ "maximum": 100,
+ "default": 20,
+ "type": "number"
+ }
+ },
+ {
+ "name": "cursor",
+ "required": false,
+ "in": "query",
+ "description": "Cursor for the next page of intents",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "offset",
+ "required": false,
+ "in": "query",
+ "description": "Number of results to skip",
+ "schema": {
+ "minimum": 0,
+ "default": 0,
+ "type": "number"
+ }
+ }
+ ],
+ "responses": {
+ "400": {
+ "description": "Invalid limit or offset"
+ }
+ },
+ "tags": [
+ "intents"
+ ]
+ },
+ "post": {
+ "operationId": "IntentsController_create",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CreateIntentDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "400": {
+ "description": "Invalid request body"
+ },
+ "409": {
+ "description": "Open-intent cap reached — a single user may not hold more than 50 open/accepted intents simultaneously"
+ },
+ "429": {
+ "description": "Rate limit exceeded — max 10 intent creations per user per 60 s (or 100 req/min per IP globally)"
+ }
+ },
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/open": {
+ "get": {
+ "operationId": "IntentsController_listOpen",
+ "parameters": [
+ {
+ "name": "state",
+ "required": false,
+ "in": "query",
+ "description": "Filter by intent state",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "open",
+ "accepted",
+ "filled",
+ "cancelled",
+ "expired",
+ "slashed",
+ "pending_open",
+ "pending_accepted",
+ "pending_filled",
+ "pending_cancelled"
+ ]
+ }
+ },
+ {
+ "name": "user",
+ "required": false,
+ "in": "query",
+ "description": "Filter by user address",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "chain",
+ "required": false,
+ "in": "query",
+ "description": "Filter by source chain",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "stellar",
+ "ethereum",
+ "base",
+ "polygon",
+ "arbitrum",
+ "optimism",
+ "avalanche"
+ ]
+ }
+ },
+ {
+ "name": "limit",
+ "required": false,
+ "in": "query",
+ "description": "Number of results per page (max 100)",
+ "schema": {
+ "minimum": 1,
+ "maximum": 100,
+ "default": 20,
+ "type": "number"
+ }
+ },
+ {
+ "name": "cursor",
+ "required": false,
+ "in": "query",
+ "description": "Cursor for the next page of intents",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "offset",
+ "required": false,
+ "in": "query",
+ "description": "Number of results to skip",
+ "schema": {
+ "minimum": 0,
+ "default": 0,
+ "type": "number"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/user/{address}": {
+ "get": {
+ "operationId": "IntentsController_listByUser",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "state",
+ "required": false,
+ "in": "query",
+ "description": "Filter by intent state",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "open",
+ "accepted",
+ "filled",
+ "cancelled",
+ "expired",
+ "slashed",
+ "pending_open",
+ "pending_accepted",
+ "pending_filled",
+ "pending_cancelled"
+ ]
+ }
+ },
+ {
+ "name": "user",
+ "required": false,
+ "in": "query",
+ "description": "Filter by user address",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "chain",
+ "required": false,
+ "in": "query",
+ "description": "Filter by source chain",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "stellar",
+ "ethereum",
+ "base",
+ "polygon",
+ "arbitrum",
+ "optimism",
+ "avalanche"
+ ]
+ }
+ },
+ {
+ "name": "limit",
+ "required": false,
+ "in": "query",
+ "description": "Number of results per page (max 100)",
+ "schema": {
+ "minimum": 1,
+ "maximum": 100,
+ "default": 20,
+ "type": "number"
+ }
+ },
+ {
+ "name": "cursor",
+ "required": false,
+ "in": "query",
+ "description": "Cursor for the next page of intents",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "offset",
+ "required": false,
+ "in": "query",
+ "description": "Number of results to skip",
+ "schema": {
+ "minimum": 0,
+ "default": 0,
+ "type": "number"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/{id}": {
+ "get": {
+ "operationId": "IntentsController_getOne",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "404": {
+ "description": "Intent not found"
+ }
+ },
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/{id}/audit": {
+ "get": {
+ "description": "Returns the full state-transition history for an intent ordered oldest-first. Each entry records the state the intent moved into, who triggered it, and why.",
+ "operationId": "IntentsController_getAudit",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "state",
+ "required": false,
+ "in": "query",
+ "description": "Filter by intent state",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "open",
+ "accepted",
+ "filled",
+ "cancelled",
+ "expired",
+ "slashed",
+ "pending_open",
+ "pending_accepted",
+ "pending_filled",
+ "pending_cancelled"
+ ]
+ }
+ },
+ {
+ "name": "user",
+ "required": false,
+ "in": "query",
+ "description": "Filter by user address",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "chain",
+ "required": false,
+ "in": "query",
+ "description": "Filter by source chain",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "stellar",
+ "ethereum",
+ "base",
+ "polygon",
+ "arbitrum",
+ "optimism",
+ "avalanche"
+ ]
+ }
+ },
+ {
+ "name": "limit",
+ "required": false,
+ "in": "query",
+ "description": "Number of results per page (max 100)",
+ "schema": {
+ "minimum": 1,
+ "maximum": 100,
+ "default": 20,
+ "type": "number"
+ }
+ },
+ {
+ "name": "cursor",
+ "required": false,
+ "in": "query",
+ "description": "Cursor for the next page of intents",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "offset",
+ "required": false,
+ "in": "query",
+ "description": "Number of results to skip",
+ "schema": {
+ "minimum": 0,
+ "default": 0,
+ "type": "number"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Audit trail for the intent",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "intentId": {
+ "type": "string"
+ },
+ "entries": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "toState": {
+ "type": "string"
+ },
+ "actor": {
+ "type": "string"
+ },
+ "reason": {
+ "type": "string"
+ },
+ "metadata": {
+ "type": "object",
+ "nullable": true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Intent not found"
+ }
+ },
+ "summary": "Get audit trail for an intent",
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/{id}/quote": {
+ "get": {
+ "operationId": "IntentsController_getPersistedQuote",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Persisted quote for the intent"
+ },
+ "404": {
+ "description": "Intent not found or no quote persisted"
+ }
+ },
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/batch": {
+ "post": {
+ "description": "Returns the current record for each supplied intent ID. IDs with no matching record are omitted (not individually 404'd). Capped at 100 IDs.",
+ "operationId": "IntentsController_batchLookup",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BatchLookupDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Records for the found intent IDs, plus a count"
+ },
+ "400": {
+ "description": "intentIds missing, not an array of strings, or exceeds 100 entries"
+ }
+ },
+ "summary": "Batch-fetch current intent records by ID",
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/batch-create": {
+ "post": {
+ "description": "Creates up to 50 intents in a single atomic (all-or-nothing) request. If any item fails validation or exceeds open intent limits, zero intents are created and per-item errors are reported.",
+ "operationId": "IntentsController_batchCreate",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BatchCreateIntentsDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BatchCreateIntentsResponseDto"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid request payload or empty batch"
+ },
+ "422": {
+ "description": "Per-item validation errors or limits exceeded"
+ }
+ },
+ "summary": "Create multiple intents atomically",
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/{id}/accept": {
+ "post": {
+ "operationId": "IntentsController_accept",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AcceptIntentDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "403": {
+ "description": "Solver not registered or inactive"
+ },
+ "404": {
+ "description": "Intent not found"
+ },
+ "409": {
+ "description": "Intent is not in open state"
+ },
+ "410": {
+ "description": "Intent has expired"
+ }
+ },
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/{id}/fill": {
+ "post": {
+ "operationId": "IntentsController_fill",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FillIntentDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "400": {
+ "description": "Fill amount below minimum"
+ },
+ "403": {
+ "description": "Wrong solver for this intent"
+ },
+ "404": {
+ "description": "Intent not found"
+ },
+ "409": {
+ "description": "Intent is not in accepted state"
+ },
+ "410": {
+ "description": "Fill window has expired"
+ },
+ "503": {
+ "description": "An emergency kill-switch is active for this intent's scope (503 + Retry-After)"
+ }
+ },
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/{id}/cancel": {
+ "post": {
+ "operationId": "IntentsController_cancel",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CancelIntentDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "403": {
+ "description": "Unauthorized"
+ },
+ "404": {
+ "description": "Intent not found"
+ },
+ "409": {
+ "description": "Intent is not in open state"
+ }
+ },
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/{id}/amend": {
+ "post": {
+ "operationId": "IntentsController_amend",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AmendIntentDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "403": {
+ "description": "Unauthorized"
+ },
+ "404": {
+ "description": "Intent not found"
+ },
+ "409": {
+ "description": "Intent is not amendable"
+ }
+ },
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/quote": {
+ "post": {
+ "operationId": "IntentsController_quote",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/QuoteRequestDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/QuoteResponseDto"
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Rate limit exceeded — max 20 quote requests per 60 s per IP"
+ }
+ },
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/intents/{id}/requote": {
+ "post": {
+ "operationId": "IntentsController_requote",
+ "parameters": [
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/QuoteResponseDto"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Intent not found"
+ },
+ "409": {
+ "description": "Intent is not in the open state"
+ },
+ "429": {
+ "description": "Rate limit exceeded — max 20 quote requests per 60 s per IP"
+ }
+ },
+ "summary": "Re-quote an existing open intent using its stored fields",
+ "tags": [
+ "intents"
+ ]
+ }
+ },
+ "/docs/ws": {
+ "get": {
+ "description": "Returns the AsyncAPI 2.6 YAML document describing the vortex.v1 subprotocol. Use this with AsyncAPI Studio or SDK generators.",
+ "operationId": "WsDocsController_getSpec",
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "summary": "AsyncAPI specification for the Vortex WebSocket feed",
+ "tags": [
+ "docs"
+ ]
+ }
+ },
+ "/solvers": {
+ "post": {
+ "operationId": "SolversController_register",
+ "parameters": [],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RegisterSolverDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ },
+ "get": {
+ "operationId": "SolversController_getLegacyLeaderboard",
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ }
+ },
+ "/solvers/leaderboard": {
+ "get": {
+ "description": "Returns the ranked solver list for a specific window. This endpoint is intended for recent-performance visibility and does not alter the legacy all-time leaderboard.",
+ "operationId": "SolversController_getLeaderboard",
+ "parameters": [
+ {
+ "name": "window",
+ "required": false,
+ "in": "query",
+ "schema": {
+ "enum": [
+ "24h",
+ "7d",
+ "30d",
+ "all"
+ ],
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "summary": "Windowed solver leaderboard",
+ "tags": [
+ "solvers"
+ ]
+ }
+ },
+ "/solvers/{address}/eligible-intents": {
+ "get": {
+ "operationId": "SolversController_getEligibleIntents",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "state",
+ "required": false,
+ "in": "query",
+ "description": "Filter by intent state",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "open",
+ "accepted",
+ "filled",
+ "cancelled",
+ "expired",
+ "slashed",
+ "pending_open",
+ "pending_accepted",
+ "pending_filled",
+ "pending_cancelled"
+ ]
+ }
+ },
+ {
+ "name": "user",
+ "required": false,
+ "in": "query",
+ "description": "Filter by user address",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "chain",
+ "required": false,
+ "in": "query",
+ "description": "Filter by source chain",
+ "schema": {
+ "type": "string",
+ "enum": [
+ "stellar",
+ "ethereum",
+ "base",
+ "polygon",
+ "arbitrum",
+ "optimism",
+ "avalanche"
+ ]
+ }
+ },
+ {
+ "name": "limit",
+ "required": false,
+ "in": "query",
+ "description": "Number of results per page (max 100)",
+ "schema": {
+ "minimum": 1,
+ "maximum": 100,
+ "default": 20,
+ "type": "number"
+ }
+ },
+ {
+ "name": "cursor",
+ "required": false,
+ "in": "query",
+ "description": "Cursor for the next page of intents",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "offset",
+ "required": false,
+ "in": "query",
+ "description": "Number of results to skip",
+ "schema": {
+ "minimum": 0,
+ "default": 0,
+ "type": "number"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ }
+ },
+ "/solvers/{address}": {
+ "get": {
+ "operationId": "SolversController_getSolver",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ },
+ "patch": {
+ "operationId": "SolversController_updateSolver",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpdateSolverDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Updated solver record"
+ },
+ "400": {
+ "description": "Invalid update body"
+ },
+ "401": {
+ "description": "Missing or invalid signature"
+ },
+ "404": {
+ "description": "Solver not found"
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ }
+ },
+ "/solvers/{address}/stats": {
+ "get": {
+ "operationId": "SolversController_getSolverStats",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "window",
+ "required": true,
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ }
+ },
+ "/solvers/{address}/slashes": {
+ "get": {
+ "operationId": "SolversController_getSlashHistory",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ }
+ },
+ "/solvers/{address}/slashes/{slashId}/dispute": {
+ "post": {
+ "operationId": "SolversController_submitDispute",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "slashId",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ }
+ },
+ "/solvers/{address}/slashes/{slashId}/dispute/resolve": {
+ "post": {
+ "operationId": "SolversController_resolveDispute",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "slashId",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ }
+ },
+ "/solvers/{address}/deregister": {
+ "post": {
+ "operationId": "SolversController_deregisterSolver",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpdateSolverStatusDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ }
+ },
+ "/solvers/{address}/deactivate": {
+ "post": {
+ "operationId": "SolversController_deactivate",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpdateSolverStatusDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ }
+ },
+ "/solvers/{address}/reactivate": {
+ "post": {
+ "operationId": "SolversController_reactivate",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpdateSolverStatusDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": ""
+ }
+ },
+ "tags": [
+ "solvers"
+ ]
+ }
+ },
+ "/api/v1/admin/griefing": {
+ "get": {
+ "operationId": "SolverGriefingController_listEnforced",
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "summary": "List solvers currently under anti-griefing enforcement",
+ "tags": [
+ "admin/griefing"
+ ]
+ }
+ },
+ "/api/v1/admin/griefing/{address}": {
+ "get": {
+ "operationId": "SolverGriefingController_getSolverRecord",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "summary": "Get anti-griefing record for a solver",
+ "tags": [
+ "admin/griefing"
+ ]
+ },
+ "delete": {
+ "operationId": "SolverGriefingController_resetSolver",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "summary": "Reset a solver's anti-griefing state to ok",
+ "tags": [
+ "admin/griefing"
+ ]
+ }
+ },
+ "/api/v1/admin/griefing/{address}/audit": {
+ "get": {
+ "operationId": "SolverGriefingController_getSolverAudit",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": ""
+ }
+ },
+ "summary": "Get anti-griefing audit log for a solver",
+ "tags": [
+ "admin/griefing"
+ ]
+ }
+ },
+ "/api/v1/admin/griefing/{address}/exclude/{intentId}": {
+ "post": {
+ "operationId": "SolverGriefingController_excludeIncident",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "intentId",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": ""
+ }
+ },
+ "summary": "Exclude an incident intent from griefing ratio",
+ "tags": [
+ "admin/griefing"
+ ]
+ }
+ },
+ "/tokens": {
"get": {
- "operationId": "HealthController_check",
+ "operationId": "TokensController_getTokens",
+ "parameters": [
+ {
+ "name": "chain",
+ "required": false,
+ "in": "query",
+ "description": "Restrict the result to a single chain (e.g. `stellar`, `ethereum`, `base`). When omitted, every supported chain plus the Stellar token list is returned.",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Supported tokens. With `chain` set, `{ tokens: Token[], chain }`; without it, `tokens` is keyed by chain and `stellarTokens` holds the Stellar list.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "tokens": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "contract": {
+ "type": "string"
+ },
+ "symbol": {
+ "type": "string",
+ "example": "USDC"
+ },
+ "name": {
+ "type": "string",
+ "example": "USD Coin"
+ },
+ "decimals": {
+ "type": "number",
+ "example": 6
+ },
+ "priceUSD": {
+ "type": "number",
+ "example": 1
+ }
+ },
+ "required": [
+ "symbol",
+ "name",
+ "decimals",
+ "priceUSD"
+ ]
+ }
+ },
+ "chain": {
+ "type": "string",
+ "example": "ethereum"
+ }
+ },
+ "required": [
+ "tokens",
+ "chain"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "tokens": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "symbol": {
+ "type": "string",
+ "example": "USDC"
+ },
+ "name": {
+ "type": "string",
+ "example": "USD Coin"
+ },
+ "decimals": {
+ "type": "number",
+ "example": 6
+ },
+ "priceUSD": {
+ "type": "number",
+ "example": 1
+ }
+ },
+ "required": [
+ "address",
+ "symbol",
+ "name",
+ "decimals",
+ "priceUSD"
+ ]
+ }
+ }
+ },
+ "stellarTokens": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "contract": {
+ "type": "string"
+ },
+ "symbol": {
+ "type": "string",
+ "example": "XLM"
+ },
+ "name": {
+ "type": "string",
+ "example": "Stellar Lumens"
+ },
+ "decimals": {
+ "type": "number",
+ "example": 7
+ },
+ "priceUSD": {
+ "type": "number",
+ "example": 0.1182
+ }
+ },
+ "required": [
+ "contract",
+ "symbol",
+ "name",
+ "decimals",
+ "priceUSD"
+ ]
+ }
+ }
+ },
+ "required": [
+ "tokens",
+ "stellarTokens"
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ "tokens"
+ ]
+ }
+ },
+ "/tokens/stellar": {
+ "get": {
+ "operationId": "TokensController_getStellarTokens",
"parameters": [],
"responses": {
"200": {
- "description": ""
+ "description": "The full list of supported Stellar destination tokens.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StellarTokensResponseDto"
+ }
+ }
+ }
}
},
"tags": [
- "health"
+ "tokens"
]
}
},
- "/api/v1/chain/health": {
+ "/chain/health": {
"get": {
"operationId": "SorobanController_getHealth",
"parameters": [],
@@ -54,7 +1716,7 @@
]
}
},
- "/api/v1/chain/ledger": {
+ "/chain/ledger": {
"get": {
"operationId": "SorobanController_getLatestLedger",
"parameters": [],
@@ -91,7 +1753,7 @@
]
}
},
- "/api/v1/chain/network": {
+ "/chain/network": {
"get": {
"operationId": "SorobanController_getNetwork",
"parameters": [],
@@ -128,7 +1790,7 @@
]
}
},
- "/api/v1/chain/account/{publicKey}": {
+ "/chain/account/{publicKey}": {
"get": {
"operationId": "SorobanController_getAccount",
"parameters": [
@@ -158,339 +1820,445 @@
"type": "string",
"example": "987654321"
},
- "balances": {
+ "balances": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "balance": {
+ "type": "string"
+ },
+ "asset_type": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "required": [
+ "id",
+ "sequence"
+ ]
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid Stellar public key format"
+ },
+ "429": {
+ "description": "Per-account rate limit exceeded (AccountRateLimitGuard)"
+ }
+ },
+ "tags": [
+ "chain"
+ ]
+ }
+ },
+ "/admin/shadow-report": {
+ "get": {
+ "description": "Returns (expected, simulated) comparison counts for on-chain simulations run in parallel with the off-chain intent path, broken down by transition and by divergence reason, plus a per-UTC-day series and shadow queue health. Headline totals are lifetime-to-date; `days` bounds only the per-day breakdown.",
+ "operationId": "ShadowController_shadowReport",
+ "parameters": [
+ {
+ "name": "days",
+ "required": false,
+ "in": "query",
+ "description": "Trailing UTC days of per-day breakdown to include (1-90, default 1).",
+ "schema": {
+ "type": "number"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Shadow-mode divergence report",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "sampleRate": {
+ "type": "number"
+ },
+ "day": {
+ "type": "string",
+ "example": "2026-09-30"
+ },
+ "generatedAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "compared": {
+ "type": "number"
+ },
+ "diverged": {
+ "type": "number"
+ },
+ "divergenceRate": {
+ "type": "number"
+ },
+ "transitions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "transition": {
+ "type": "string",
+ "example": "accept"
+ },
+ "compared": {
+ "type": "number"
+ },
+ "diverged": {
+ "type": "number"
+ },
+ "divergenceRate": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "divergences": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "transition": {
+ "type": "string",
+ "example": "fill"
+ },
+ "reason": {
+ "type": "string",
+ "example": "outcome_mismatch",
+ "enum": [
+ "outcome_mismatch",
+ "simulation_error",
+ "simulation_exception",
+ "contract_unconfigured"
+ ]
+ },
+ "count": {
+ "type": "number"
+ }
+ }
+ }
+ },
+ "daily": {
"type": "array",
"items": {
"type": "object",
"properties": {
- "balance": {
- "type": "string"
+ "day": {
+ "type": "string",
+ "example": "2026-09-30"
},
- "asset_type": {
- "type": "string"
+ "compared": {
+ "type": "number"
+ },
+ "diverged": {
+ "type": "number"
+ },
+ "divergenceRate": {
+ "type": "number"
+ },
+ "cells": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
}
}
}
+ },
+ "queue": {
+ "type": "object",
+ "properties": {
+ "depth": {
+ "type": "number"
+ },
+ "capacity": {
+ "type": "number"
+ },
+ "dropped": {
+ "type": "number"
+ },
+ "sampledOut": {
+ "type": "number"
+ },
+ "disabled": {
+ "type": "number"
+ },
+ "completed": {
+ "type": "number"
+ }
+ }
}
- },
- "required": [
- "id",
- "sequence"
- ]
+ }
}
}
}
- },
- "400": {
- "description": "Invalid Stellar public key format"
- },
- "429": {
- "description": "Per-account rate limit exceeded (AccountRateLimitGuard)"
}
},
+ "summary": "Daily shadow-mode divergence summary",
"tags": [
- "chain"
+ "admin"
]
}
},
- "/api/v1/tokens": {
+ "/params": {
"get": {
- "operationId": "TokensController_getTokens",
- "parameters": [
- {
- "name": "chain",
- "required": false,
- "in": "query",
- "description": "Restrict the result to a single chain (e.g. `stellar`, `ethereum`, `base`). When omitted, every supported chain plus the Stellar token list is returned.",
- "schema": {
- "type": "string"
- }
- }
- ],
+ "description": "Returns the actively-enforced protocol parameters, any pending governance change with its timelock execution ledger and ETA, and recent parameter history. Parameters are sourced from the on-chain governance contract when PARAMS_CONTRACT_ID is configured; code/env defaults are used otherwise.",
+ "operationId": "ParamsController_getParams",
+ "parameters": [],
"responses": {
"200": {
- "description": "Supported tokens. With `chain` set, `{ tokens: Token[], chain }`; without it, `tokens` is keyed by chain and `stellarTokens` holds the Stellar list.",
+ "description": "Protocol parameters — current, pending, and history",
"content": {
"application/json": {
"schema": {
- "oneOf": [
- {
+ "type": "object",
+ "properties": {
+ "current": {
"type": "object",
+ "description": "Currently active protocol parameters",
"properties": {
- "tokens": {
- "type": "array",
- "items": {
+ "version": {
+ "type": "number",
+ "example": 3
+ },
+ "feeBps": {
+ "type": "number",
+ "example": 30,
+ "description": "Protocol fee in basis points"
+ },
+ "chains": {
+ "type": "object",
+ "description": "Per-chain deadline and fill-window overrides",
+ "additionalProperties": {
"type": "object",
"properties": {
- "address": {
- "type": "string"
- },
- "contract": {
- "type": "string"
- },
- "symbol": {
- "type": "string",
- "example": "USDC"
- },
- "name": {
- "type": "string",
- "example": "USD Coin"
- },
- "decimals": {
+ "deadlineSeconds": {
"type": "number",
- "example": 6
+ "example": 900
},
- "priceUSD": {
+ "fillWindowSeconds": {
"type": "number",
- "example": 1
+ "example": 120
}
- },
- "required": [
- "symbol",
- "name",
- "decimals",
- "priceUSD"
- ]
+ }
}
},
- "chain": {
+ "maxExposureRatio": {
+ "type": "number",
+ "example": 0.05,
+ "description": "Maximum on-chain exposure ratio (0–1)"
+ },
+ "slashAmount": {
"type": "string",
- "example": "ethereum"
+ "example": "100000000"
+ },
+ "activeSinceLedger": {
+ "type": "number",
+ "example": 12345678
+ },
+ "adoptedAt": {
+ "type": "string",
+ "format": "date-time"
}
},
"required": [
- "tokens",
- "chain"
+ "version",
+ "feeBps",
+ "chains",
+ "maxExposureRatio",
+ "slashAmount",
+ "activeSinceLedger",
+ "adoptedAt"
]
},
- {
- "type": "object",
- "properties": {
- "tokens": {
+ "pending": {
+ "nullable": true,
+ "description": "Scheduled governance change not yet activated, or null",
+ "oneOf": [
+ {
"type": "object",
- "additionalProperties": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "address": {
- "type": "string"
- },
- "symbol": {
- "type": "string",
- "example": "USDC"
- },
- "name": {
- "type": "string",
- "example": "USD Coin"
- },
- "decimals": {
- "type": "number",
- "example": 6
- },
- "priceUSD": {
- "type": "number",
- "example": 1
- }
- },
- "required": [
- "address",
- "symbol",
- "name",
- "decimals",
- "priceUSD"
- ]
+ "properties": {
+ "params": {
+ "type": "object"
+ },
+ "executionLedger": {
+ "type": "number",
+ "example": 12349999
+ },
+ "estimatedEta": {
+ "type": "string",
+ "format": "date-time",
+ "description": "Best-effort ETA for timelock execution"
+ },
+ "observedAt": {
+ "type": "string",
+ "format": "date-time"
}
}
},
- "stellarTokens": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "contract": {
- "type": "string"
- },
- "symbol": {
- "type": "string",
- "example": "XLM"
- },
- "name": {
- "type": "string",
- "example": "Stellar Lumens"
- },
- "decimals": {
- "type": "number",
- "example": 7
- },
- "priceUSD": {
- "type": "number",
- "example": 0.1182
- }
- },
- "required": [
- "contract",
- "symbol",
- "name",
- "decimals",
- "priceUSD"
- ]
- }
+ {
+ "type": "null"
}
- },
- "required": [
- "tokens",
- "stellarTokens"
]
+ },
+ "history": {
+ "type": "array",
+ "description": "Previous parameter versions, newest-first (max 50)",
+ "items": {
+ "type": "object"
+ }
}
+ },
+ "required": [
+ "current",
+ "pending",
+ "history"
]
}
}
}
}
},
+ "summary": "Current and pending governance-controlled protocol parameters",
"tags": [
- "tokens"
+ "governance"
]
}
},
- "/api/v1/tokens/stellar": {
+ "/governance/guardian/status": {
"get": {
- "operationId": "TokensController_getStellarTokens",
+ "operationId": "GuardianController_status",
"parameters": [],
"responses": {
"200": {
- "description": "The full list of supported Stellar destination tokens.",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/StellarTokensResponseDto"
- }
- }
- }
+ "description": ""
}
},
+ "summary": "Active guardian actions (with tx references) and effective pause state",
"tags": [
- "tokens"
+ "governance"
]
}
},
- "/api/v1/intents": {
- "get": {
- "operationId": "IntentsController_list",
+ "/governance/guardian/actions/{id}/override": {
+ "post": {
+ "operationId": "GuardianController_override",
"parameters": [
{
- "name": "state",
- "required": false,
- "in": "query",
- "description": "Filter by intent state",
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "user",
- "required": false,
- "in": "query",
- "description": "Filter by user address",
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "chain",
- "required": false,
- "in": "query",
- "description": "Filter by source chain",
+ "name": "id",
+ "required": true,
+ "in": "path",
"schema": {
"type": "string"
}
},
{
- "name": "limit",
+ "name": "x-admin-key",
+ "in": "header",
"required": true,
- "in": "query",
- "description": "Number of results per page",
- "schema": {
- "minimum": 1,
- "maximum": 100,
- "default": 20,
- "type": "number"
- }
- },
- {
- "name": "cursor",
- "required": false,
- "in": "query",
- "description": "Cursor for the next page of intents",
"schema": {
"type": "string"
}
- },
- {
- "name": "offset",
- "required": true,
- "in": "query",
- "description": "Number of results to skip",
- "schema": {
- "minimum": 0,
- "default": 0,
- "type": "number"
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/GuardianOverrideDto"
+ }
}
}
- ],
+ },
"responses": {
- "400": {
- "description": "Invalid limit or offset"
+ "200": {
+ "description": ""
}
},
+ "summary": "Superadmin override of an active guardian action (audited)",
"tags": [
- "intents"
+ "governance"
]
- },
+ }
+ },
+ "/solvers/{address}/credentials": {
"post": {
- "operationId": "IntentsController_create",
- "parameters": [],
+ "description": "Mints a new scoped credential for the authenticated solver. The plaintext secret is returned ONCE. Requires a valid SEP-10 JWT for the solver.",
+ "operationId": "SolverCredentialController_create",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateIntentDto"
+ "$ref": "#/components/schemas/CreateSolverCredentialDto"
}
}
}
},
"responses": {
- "400": {
- "description": "Invalid request body"
+ "201": {
+ "description": "Credential created (plaintext shown once)"
},
- "429": {
- "description": "Rate limit exceeded — max 10 intent creations per user per 60 s (or 100 req/min per IP globally)"
+ "401": {
+ "description": "Missing or invalid SEP-10 JWT"
}
},
+ "summary": "Create a scoped solver credential",
"tags": [
- "intents"
+ "solvers"
]
- }
- },
- "/api/v1/intents/open": {
+ },
"get": {
- "operationId": "IntentsController_listOpen",
- "parameters": [],
+ "description": "Lists credential metadata (never plaintext) for the authenticated solver.",
+ "operationId": "SolverCredentialController_list",
+ "parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
"responses": {
"200": {
- "description": ""
+ "description": "Credential metadata list"
+ },
+ "401": {
+ "description": "Missing or invalid SEP-10 JWT"
}
},
+ "summary": "List a solver's credentials",
"tags": [
- "intents"
+ "solvers"
]
}
},
- "/api/v1/intents/user/{address}": {
- "get": {
- "operationId": "IntentsController_listByUser",
+ "/solvers/{address}/credentials/{id}/rotate": {
+ "post": {
+ "description": "Revokes the credential and issues a replacement with the same scopes. The new plaintext is returned ONCE.",
+ "operationId": "SolverCredentialController_rotate",
"parameters": [
{
"name": "address",
@@ -499,22 +2267,53 @@
"schema": {
"type": "string"
}
+ },
+ {
+ "name": "id",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
}
],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CreateSolverCredentialDto"
+ }
+ }
+ }
+ },
"responses": {
"200": {
- "description": ""
+ "description": "Credential rotated (new plaintext shown once)"
+ },
+ "401": {
+ "description": "Missing or invalid SEP-10 JWT"
}
},
+ "summary": "Rotate a solver credential",
"tags": [
- "intents"
+ "solvers"
]
}
},
- "/api/v1/intents/{id}": {
- "get": {
- "operationId": "IntentsController_getOne",
+ "/solvers/{address}/credentials/{id}/revoke": {
+ "post": {
+ "description": "Instantly revokes the credential and propagates to all replicas.",
+ "operationId": "SolverCredentialController_revoke",
"parameters": [
+ {
+ "name": "address",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ },
{
"name": "id",
"required": true,
@@ -525,24 +2324,27 @@
}
],
"responses": {
- "404": {
- "description": "Intent not found"
+ "200": {
+ "description": "Credential revoked"
+ },
+ "401": {
+ "description": "Missing or invalid SEP-10 JWT"
}
},
+ "summary": "Revoke a solver credential",
"tags": [
- "intents"
+ "solvers"
]
}
},
- "/api/v1/intents/{id}/audit": {
+ "/admin/jobs/queues": {
"get": {
- "description": "Returns the full state-transition history for an intent ordered oldest-first. Each entry records the state the intent moved into, who triggered it, and why.",
- "operationId": "IntentsController_getAudit",
+ "operationId": "JobsController_queues",
"parameters": [
{
- "name": "id",
+ "name": "x-admin-key",
+ "in": "header",
"required": true,
- "in": "path",
"schema": {
"type": "string"
}
@@ -550,61 +2352,29 @@
],
"responses": {
"200": {
- "description": "Audit trail for the intent",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "intentId": {
- "type": "string"
- },
- "entries": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "timestamp": {
- "type": "string",
- "format": "date-time"
- },
- "toState": {
- "type": "string"
- },
- "actor": {
- "type": "string"
- },
- "reason": {
- "type": "string"
- },
- "metadata": {
- "type": "object",
- "nullable": true
- }
- }
- }
- }
- }
- }
- }
- }
- },
- "404": {
- "description": "Intent not found"
+ "description": ""
}
},
- "summary": "Get audit trail for an intent",
+ "summary": "Depth and dead-letter counts per queue",
"tags": [
- "intents"
+ "admin"
]
}
},
- "/api/v1/intents/{id}/quote": {
+ "/admin/jobs/queues/{queue}/dead-letters": {
"get": {
- "operationId": "IntentsController_getPersistedQuote",
+ "operationId": "JobsController_deadLetters",
"parameters": [
{
- "name": "id",
+ "name": "x-admin-key",
+ "in": "header",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "queue",
"required": true,
"in": "path",
"schema": {
@@ -614,56 +2384,54 @@
],
"responses": {
"200": {
- "description": "Persisted quote for the intent"
- },
- "404": {
- "description": "Intent not found or no quote persisted"
+ "description": ""
}
},
+ "summary": "Most recent dead-lettered jobs for a queue",
"tags": [
- "intents"
+ "admin"
]
}
},
- "/api/v1/intents/{id}/auction": {
+ "/admin/flags": {
"get": {
- "operationId": "IntentsController_getAuctionPrice",
+ "operationId": "FlagsController_list",
"parameters": [
{
- "name": "id",
+ "name": "x-admin-key",
+ "in": "header",
"required": true,
- "in": "path",
- "schema": { "type": "string" }
+ "schema": {
+ "type": "string"
+ }
}
],
"responses": {
"200": {
- "description": "Current and accepted Dutch auction prices",
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "intentId": { "type": "string" },
- "currentDstAmount": { "type": "string" },
- "acceptedDstAmount": { "type": "string" },
- "timestamp": { "type": "number" }
- }
- }
- }
- }
- },
- "404": { "description": "Intent not found or does not use a Dutch auction" }
+ "description": ""
+ }
},
- "tags": ["intents"]
+ "summary": "Managed flags with env default, override pin and stored rules",
+ "tags": [
+ "admin"
+ ]
}
},
- "/api/v1/intents/{id}/accept": {
- "post": {
- "operationId": "IntentsController_accept",
+ "/admin/flags/{key}": {
+ "put": {
+ "description": "Returns 202-style { status: 'pending' } when a second approval is required.",
+ "operationId": "FlagsController_update",
"parameters": [
{
- "name": "id",
+ "name": "x-admin-key",
+ "in": "header",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "key",
"required": true,
"in": "path",
"schema": {
@@ -676,34 +2444,34 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/AcceptIntentDto"
+ "$ref": "#/components/schemas/UpdateFlagDto"
}
}
}
},
"responses": {
- "403": {
- "description": "Solver not registered or inactive"
- },
- "404": {
- "description": "Intent not found"
- },
- "409": {
- "description": "Intent is not in open state"
- },
- "410": {
- "description": "Intent has expired"
+ "200": {
+ "description": ""
}
},
+ "summary": "Set a flag's default and targeting rules",
"tags": [
- "intents"
+ "admin"
]
}
},
- "/api/v1/intents/{id}/fill": {
+ "/admin/flags/change-requests/{id}/approve": {
"post": {
- "operationId": "IntentsController_fill",
+ "operationId": "FlagsController_approve",
"parameters": [
+ {
+ "name": "x-admin-key",
+ "in": "header",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
{
"name": "id",
"required": true,
@@ -713,44 +2481,31 @@
}
}
],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/FillIntentDto"
- }
- }
- }
- },
"responses": {
- "400": {
- "description": "Fill amount below minimum"
- },
- "403": {
- "description": "Wrong solver for this intent"
- },
- "404": {
- "description": "Intent not found"
- },
- "409": {
- "description": "Intent is not in accepted state"
- },
- "410": {
- "description": "Fill window has expired"
+ "200": {
+ "description": ""
}
},
+ "summary": "Approve a pending change (must be a different admin than the proposer)",
"tags": [
- "intents"
+ "admin"
]
}
},
- "/api/v1/intents/{id}/cancel": {
- "post": {
- "operationId": "IntentsController_cancel",
+ "/admin/flags/{key}/audit": {
+ "get": {
+ "operationId": "FlagsController_auditLog",
"parameters": [
{
- "name": "id",
+ "name": "x-admin-key",
+ "in": "header",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "key",
"required": true,
"in": "path",
"schema": {
@@ -758,91 +2513,48 @@
}
}
],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/CancelIntentDto"
- }
- }
- }
- },
"responses": {
- "403": {
- "description": "Unauthorized"
- },
- "404": {
- "description": "Intent not found"
- },
- "409": {
- "description": "Intent is not in open state"
+ "200": {
+ "description": ""
}
},
+ "summary": "Audit trail for a flag, newest first",
"tags": [
- "intents"
+ "admin"
]
}
},
- "/api/v1/intents/quote": {
- "post": {
- "operationId": "IntentsController_quote",
+ "/health/live": {
+ "get": {
+ "operationId": "HealthController_live",
"parameters": [],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/QuoteRequestDto"
- }
- }
- }
- },
"responses": {
"200": {
- "description": "",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/QuoteResponseDto"
- }
- }
- }
- },
- "429": {
- "description": "Rate limit exceeded — max 20 quote requests per 60 s per IP"
+ "description": ""
}
},
"tags": [
- "intents"
+ "health"
]
}
},
- "/api/v1/solvers": {
- "post": {
- "operationId": "SolversController_register",
+ "/health/ready": {
+ "get": {
+ "operationId": "HealthController_ready",
"parameters": [],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RegisterSolverDto"
- }
- }
- }
- },
"responses": {
- "201": {
+ "200": {
"description": ""
}
},
"tags": [
- "solvers"
+ "health"
]
- },
+ }
+ },
+ "/health/startup": {
"get": {
- "operationId": "SolversController_getLeaderboard",
+ "operationId": "HealthController_startup",
"parameters": [],
"responses": {
"200": {
@@ -850,190 +2562,335 @@
}
},
"tags": [
- "solvers"
+ "health"
]
}
},
- "/api/v1/solvers/{address}": {
+ "/health": {
"get": {
- "operationId": "SolversController_getSolver",
- "parameters": [
- {
- "name": "address",
- "required": true,
- "in": "path",
- "schema": {
- "type": "string"
- }
- }
- ],
+ "operationId": "HealthController_check",
+ "parameters": [],
"responses": {
"200": {
"description": ""
}
},
"tags": [
- "solvers"
+ "health"
]
- },
- "patch": {
- "operationId": "SolversController_updateSolver",
- "parameters": [
- {
- "name": "address",
- "required": true,
- "in": "path",
- "schema": {
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "required": true,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/UpdateSolverDto"
- }
- }
+ }
+ },
+ "/stats": {
+ "get": {
+ "operationId": "StatsController_getStats",
+ "parameters": [],
+ "responses": {
+ "200": {
+ "description": ""
}
},
+ "tags": [
+ "stats"
+ ]
+ }
+ },
+ "/stats/treasury": {
+ "get": {
+ "operationId": "StatsController_getTreasuryStats",
+ "parameters": [],
"responses": {
"200": {
- "description": "Updated solver record"
- },
- "400": {
- "description": "Invalid update body"
- },
- "401": {
- "description": "Missing or invalid signature"
- },
- "404": {
- "description": "Solver not found"
+ "description": ""
}
},
"tags": [
- "solvers"
+ "stats"
]
}
},
- "/api/v1/solvers/{address}/stats": {
+ "/stats/ws": {
"get": {
- "operationId": "SolversController_getSolverStats",
- "parameters": [
- {
- "name": "address",
- "required": true,
- "in": "path",
- "schema": {
- "type": "string"
- }
- }
- ],
+ "operationId": "StatsController_getWsStats",
+ "parameters": [],
"responses": {
"200": {
"description": ""
}
},
"tags": [
- "solvers"
+ "stats"
]
}
},
- "/api/v1/solvers/{address}/deregister": {
- "post": {
- "operationId": "SolversController_deregisterSolver",
+ "/treasury/reconciliation": {
+ "get": {
+ "description": "Returns daily reconciliation summary showing expected vs actual balances per asset",
+ "operationId": "TreasuryController_getReconciliation",
"parameters": [
{
- "name": "address",
- "required": true,
- "in": "path",
+ "name": "date",
+ "required": false,
+ "in": "query",
+ "description": "Date in YYYY-MM-DD format (defaults to today)",
"schema": {
+ "example": "2026-09-28",
"type": "string"
}
}
],
"responses": {
- "201": {
- "description": ""
+ "200": {
+ "description": "Reconciliation summary",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "date": {
+ "type": "string",
+ "example": "2026-09-28"
+ },
+ "assets": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "asset": {
+ "type": "string",
+ "example": "native"
+ },
+ "expectedBalance": {
+ "type": "string",
+ "example": "1000000000"
+ },
+ "actualBalance": {
+ "type": "string",
+ "example": "1000500000"
+ },
+ "discrepancy": {
+ "type": "string",
+ "example": "500000"
+ },
+ "discrepancyPercentage": {
+ "type": "number",
+ "example": 0.05
+ },
+ "hasUnexplainedDiscrepancy": {
+ "type": "boolean",
+ "example": false
+ },
+ "explanation": {
+ "type": "string",
+ "nullable": true
+ }
+ }
+ }
+ },
+ "totalDiscrepancies": {
+ "type": "number",
+ "example": 3
+ },
+ "assetsWithUnexplainedDiscrepancies": {
+ "type": "number",
+ "example": 1
+ },
+ "lastReconciliationAt": {
+ "type": "string",
+ "example": "2026-09-28T00:00:00.000Z"
+ }
+ }
+ }
+ }
+ }
}
},
+ "summary": "Get treasury reconciliation summary",
"tags": [
- "solvers"
+ "treasury"
]
}
},
- "/api/v1/solvers/{address}/deactivate": {
- "post": {
- "operationId": "SolversController_deactivate",
+ "/treasury/reconciliation/{asset}": {
+ "get": {
+ "description": "Returns detailed reconciliation data including transaction breakdown",
+ "operationId": "TreasuryController_getAssetReconciliation",
"parameters": [
{
- "name": "address",
+ "name": "asset",
"required": true,
"in": "path",
+ "description": "Asset identifier (e.g., 'native', 'USDC:ISSUER', or contract address)",
+ "schema": {
+ "example": "native",
+ "type": "string"
+ }
+ },
+ {
+ "name": "date",
+ "required": false,
+ "in": "query",
+ "description": "Date in YYYY-MM-DD format (defaults to today)",
"schema": {
+ "example": "2026-09-28",
"type": "string"
}
}
],
"responses": {
- "201": {
- "description": ""
+ "200": {
+ "description": "Detailed reconciliation data",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "snapshotDate": {
+ "type": "string",
+ "example": "2026-09-28"
+ },
+ "asset": {
+ "type": "string",
+ "example": "native"
+ },
+ "expectedBalance": {
+ "type": "string",
+ "example": "1000000000"
+ },
+ "actualBalance": {
+ "type": "string",
+ "example": "1000500000"
+ },
+ "discrepancy": {
+ "type": "string",
+ "example": "500000"
+ },
+ "discrepancyPercentage": {
+ "type": "number",
+ "example": 0.05
+ },
+ "toleranceThreshold": {
+ "type": "string",
+ "example": "10000000"
+ },
+ "hasUnexplainedDiscrepancy": {
+ "type": "boolean",
+ "example": false
+ },
+ "explanation": {
+ "type": "string",
+ "nullable": true
+ },
+ "breakdown": {
+ "type": "object",
+ "properties": {
+ "fees": {
+ "type": "string",
+ "example": "500000000"
+ },
+ "slashes": {
+ "type": "string",
+ "example": "100000000"
+ },
+ "refunds": {
+ "type": "string",
+ "example": "50000000"
+ }
+ }
+ },
+ "recentTransactions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "fee",
+ "slash",
+ "refund"
+ ]
+ },
+ "amount": {
+ "type": "string",
+ "example": "1000000"
+ },
+ "timestamp": {
+ "type": "string",
+ "example": "2026-09-28T12:00:00.000Z"
+ },
+ "reference": {
+ "type": "string",
+ "example": "intent-uuid"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
},
+ "summary": "Get detailed reconciliation for an asset",
"tags": [
- "solvers"
+ "treasury"
]
}
},
- "/api/v1/solvers/{address}/reactivate": {
+ "/treasury/reconciliation/trigger": {
"post": {
- "operationId": "SolversController_reactivate",
+ "description": "Admin endpoint to trigger reconciliation on-demand",
+ "operationId": "TreasuryController_triggerReconciliation",
"parameters": [
{
- "name": "address",
- "required": true,
- "in": "path",
+ "name": "asset",
+ "required": false,
+ "in": "query",
+ "description": "Specific asset to reconcile (reconciles all if omitted)",
"schema": {
+ "example": "native",
"type": "string"
}
}
],
- "responses": {
- "201": {
- "description": ""
- }
- },
- "tags": [
- "solvers"
- ]
- }
- },
- "/api/v1/stats": {
- "get": {
- "operationId": "StatsController_getStats",
- "parameters": [],
- "responses": {
- "200": {
- "description": ""
- }
- },
- "tags": [
- "stats"
- ]
- }
- },
- "/api/v1/stats/ws": {
- "get": {
- "operationId": "StatsController_getWsStats",
- "parameters": [],
"responses": {
"200": {
- "description": ""
+ "description": "Reconciliation completed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string",
+ "example": "Reconciliation completed"
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "asset": {
+ "type": "string"
+ },
+ "hasUnexplainedDiscrepancy": {
+ "type": "boolean"
+ },
+ "discrepancy": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
},
+ "summary": "Manually trigger treasury reconciliation",
"tags": [
- "stats"
+ "treasury"
]
}
}
@@ -1078,69 +2935,83 @@
],
"components": {
"schemas": {
- "StellarTokenDto": {
+ "PauseKillSwitchDto": {
"type": "object",
"properties": {
- "contract": {
+ "scope": {
"type": "string",
- "description": "Stellar contract ID of the token"
+ "enum": [
+ "global",
+ "chain",
+ "token",
+ "operation"
+ ],
+ "description": "Breadth of the pause."
},
- "symbol": {
+ "chain": {
"type": "string",
- "example": "XLM"
+ "description": "Required for chain/token/operation scope."
},
- "name": {
+ "token": {
"type": "string",
- "example": "Stellar Lumens"
+ "description": "Required for token scope; wildcard for operation scope."
},
- "decimals": {
- "type": "number",
- "example": 7
+ "operation": {
+ "type": "string",
+ "enum": [
+ "create",
+ "accept",
+ "fill",
+ "slash",
+ "onchain"
+ ],
+ "description": "Required for operation scope."
},
- "priceUSD": {
- "type": "number",
- "example": 0.1182
+ "reasonCode": {
+ "type": "string",
+ "enum": [
+ "INCIDENT",
+ "TOKEN_DEPEGGED",
+ "SOLVER_INCIDENT",
+ "CHAIN_DEGRADED",
+ "RPC_DEGRADED",
+ "REGULATORY",
+ "MAINTENANCE"
+ ]
+ },
+ "reason": {
+ "type": "string",
+ "description": "Operator explanation, shown to clients and in the audit log."
}
},
"required": [
- "contract",
- "symbol",
- "name",
- "decimals",
- "priceUSD"
+ "scope",
+ "reasonCode",
+ "reason"
]
},
- "StellarTokensResponseDto": {
+ "ApproveResumeDto": {
"type": "object",
"properties": {
- "tokens": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/StellarTokenDto"
- }
+ "note": {
+ "type": "string",
+ "description": "Why the resume is safe."
+ },
+ "approvalsRequired": {
+ "type": "number",
+ "default": 2,
+ "minimum": 2,
+ "description": "Distinct approvals needed."
}
- },
- "required": [
- "tokens"
- ]
- },
- "DutchAuctionDto": {
- "type": "object",
- "properties": {
- "startDstAmount": { "type": "string", "description": "Starting destination amount in base units" },
- "decayStart": { "type": "number", "description": "Unix timestamp when the price decay starts" },
- "decayEnd": { "type": "number", "description": "Unix timestamp when the price reaches minDstAmount" },
- "exclusiveSolver": { "type": "string", "maxLength": 56, "description": "Solver address exclusively eligible until exclusivityEnd" },
- "exclusivityEnd": { "type": "number", "description": "Unix timestamp when solver exclusivity ends" }
- },
- "required": ["startDstAmount", "decayStart", "decayEnd"]
+ }
},
"CreateIntentDto": {
"type": "object",
"properties": {
"user": {
"type": "string",
- "description": "Stellar or EVM address of the user creating the intent"
+ "description": "Stellar address of the user creating the intent",
+ "maxLength": 56
},
"srcChain": {
"type": "string",
@@ -1161,7 +3032,8 @@
},
"srcTokenSymbol": {
"type": "string",
- "description": "Source token symbol, e.g. USDC"
+ "description": "Source token symbol, e.g. USDC",
+ "maxLength": 16
},
"srcTokenDecimals": {
"type": "number",
@@ -1175,11 +3047,13 @@
},
"dstTokenContract": {
"type": "string",
- "description": "Destination Stellar token contract"
+ "description": "Destination Stellar token contract",
+ "maxLength": 56
},
"dstTokenSymbol": {
"type": "string",
- "description": "Destination token symbol, e.g. USDC"
+ "description": "Destination token symbol, e.g. USDC",
+ "maxLength": 16
},
"dstTokenDecimals": {
"type": "number",
@@ -1191,17 +3065,10 @@
"type": "string",
"description": "Minimum acceptable destination amount as an integer string"
},
- "auction": {
- "$ref": "#/components/schemas/DutchAuctionDto",
- "description": "Optional Dutch-auction allocation terms"
- },
"deadline": {
"type": "number",
"description": "Unix timestamp deadline; defaults to now + 1800s; must be between now+60s and now+24h"
},
- "signature": { "type": "string", "maxLength": 4096, "description": "EIP-712 signature for EVM-originated intent creation" },
- "nonce": { "type": "string", "maxLength": 128, "description": "Unique nonce included in the EIP-712 signature" },
- "expiresAt": { "type": "number", "description": "Unix timestamp when the EIP-712 signature expires" },
"idempotencyKey": {
"type": "string",
"description": "Idempotency key for deduplicating duplicate requests"
@@ -1220,18 +3087,93 @@
"minDstAmount"
]
},
+ "BatchLookupDto": {
+ "type": "object",
+ "properties": {
+ "intentIds": {
+ "maxItems": 100,
+ "description": "Intent IDs to look up (max 100). IDs with no matching record are omitted from the response, not individually 404'd.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": [
+ "intentIds"
+ ]
+ },
+ "BatchCreateIntentsDto": {
+ "type": "object",
+ "properties": {
+ "intents": {
+ "description": "List of intents to create atomically (1..50)",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/CreateIntentDto"
+ }
+ }
+ },
+ "required": [
+ "intents"
+ ]
+ },
+ "BatchCreateItemErrorDto": {
+ "type": "object",
+ "properties": {
+ "index": {
+ "type": "number",
+ "description": "Zero-based index of the failed intent item in the input array"
+ },
+ "field": {
+ "type": "string",
+ "description": "Field name associated with the error, if applicable"
+ },
+ "message": {
+ "type": "string",
+ "description": "Human-readable error description"
+ }
+ },
+ "required": [
+ "index",
+ "message"
+ ]
+ },
+ "BatchCreateIntentsResponseDto": {
+ "type": "object",
+ "properties": {
+ "created": {
+ "description": "Array of created Intent objects when successful",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "errors": {
+ "description": "List of per-item validation errors if any failed",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/BatchCreateItemErrorDto"
+ }
+ }
+ },
+ "required": [
+ "created",
+ "errors"
+ ]
+ },
"AcceptIntentDto": {
"type": "object",
"properties": {
"solver": {
"type": "string",
- "description": "Solver address accepting the intent"
+ "description": "Solver address accepting the intent",
+ "maxLength": 56
},
- "nonce": { "type": "string", "minLength": 16, "maxLength": 128, "description": "Single-use signing nonce" },
- "expiresAt": { "type": "number", "description": "Unix timestamp when the signature expires" },
"signature": {
"type": "string",
- "description": "Base64-encoded Ed25519 signature of the signed intent action"
+ "description": "Base64-encoded Ed25519 signature of the message \"accept::\" produced by the solver's private key",
+ "maxLength": 88
}
},
"required": [
@@ -1244,7 +3186,8 @@
"properties": {
"solver": {
"type": "string",
- "description": "Solver address filling the intent (must match the accepting solver)"
+ "description": "Solver address filling the intent (must match the accepting solver)",
+ "maxLength": 56
},
"fillAmount": {
"type": "string",
@@ -1252,13 +3195,13 @@
},
"txHash": {
"type": "string",
- "description": "Stellar fill transaction hash"
+ "description": "Stellar fill transaction hash",
+ "maxLength": 128
},
- "nonce": { "type": "string", "minLength": 16, "maxLength": 128, "description": "Single-use signing nonce" },
- "expiresAt": { "type": "number", "description": "Unix timestamp when the signature expires" },
"signature": {
"type": "string",
- "description": "Base64-encoded Ed25519 signature of the signed intent action"
+ "description": "Base64-encoded Ed25519 signature of the message \"fill::\" produced by the solver's private key",
+ "maxLength": 88
}
},
"required": [
@@ -1272,18 +3215,46 @@
"properties": {
"user": {
"type": "string",
- "description": "Stellar or EVM address of the intent's original creator (must match)"
+ "description": "Stellar address of the intent's original creator (must match)",
+ "maxLength": 56
+ },
+ "signature": {
+ "type": "string",
+ "description": "Base64-encoded Ed25519 signature of the message \"cancel:\" produced by the private key of `user`",
+ "maxLength": 88
+ }
+ },
+ "required": [
+ "user",
+ "signature"
+ ]
+ },
+ "AmendIntentDto": {
+ "type": "object",
+ "properties": {
+ "user": {
+ "type": "string",
+ "description": "Stellar address of the intent's original creator (must match)",
+ "maxLength": 56
+ },
+ "minDstAmount": {
+ "type": "string",
+ "description": "Replacement minimum destination amount in base units"
+ },
+ "deadline": {
+ "type": "number",
+ "description": "Replacement Unix timestamp deadline"
},
- "nonce": { "type": "string", "minLength": 16, "maxLength": 128, "description": "Single-use signing nonce" },
- "expiresAt": { "type": "number", "description": "Unix timestamp when the signature expires" },
"signature": {
"type": "string",
- "maxLength": 4096,
- "description": "Base64 Ed25519 or EIP-712 signature proving control of user"
+ "description": "Base64 Ed25519 signature of \"amend::::\"",
+ "maxLength": 88
}
},
"required": [
"user",
+ "minDstAmount",
+ "deadline",
"signature"
]
},
@@ -1517,6 +3488,10 @@
"priceImpact": {
"type": "number",
"description": "Price impact for the best quote as a decimal fraction (0 when no quote available)"
+ },
+ "indicative": {
+ "type": "boolean",
+ "description": "True when no solver responded and the returned quote is indicative"
}
},
"required": [
@@ -1536,11 +3511,13 @@
"properties": {
"address": {
"type": "string",
- "description": "Solver's Stellar address"
+ "description": "Solver's Stellar address",
+ "maxLength": 56
},
"name": {
"type": "string",
- "description": "Solver's display name"
+ "description": "Solver's display name",
+ "maxLength": 64
},
"bondAmount": {
"type": "string",
@@ -1575,7 +3552,8 @@
},
"proofSignature": {
"type": "string",
- "description": "Proof-of-control signature for the advertised solver address"
+ "description": "Proof-of-control signature for the advertised solver address",
+ "maxLength": 88
}
},
"required": [
@@ -1630,6 +3608,103 @@
"required": [
"signature"
]
+ },
+ "UpdateSolverStatusDto": {
+ "type": "object",
+ "properties": {
+ "signature": {
+ "type": "string",
+ "description": "Proof-of-control signature for the solver status update",
+ "maxLength": 88
+ }
+ },
+ "required": [
+ "signature"
+ ]
+ },
+ "StellarTokenDto": {
+ "type": "object",
+ "properties": {
+ "contract": {
+ "type": "string",
+ "description": "Stellar contract ID of the token"
+ },
+ "symbol": {
+ "type": "string",
+ "example": "XLM"
+ },
+ "name": {
+ "type": "string",
+ "example": "Stellar Lumens"
+ },
+ "decimals": {
+ "type": "number",
+ "example": 7
+ },
+ "priceUSD": {
+ "type": "number",
+ "example": 0.1182
+ }
+ },
+ "required": [
+ "contract",
+ "symbol",
+ "name",
+ "decimals",
+ "priceUSD"
+ ]
+ },
+ "StellarTokensResponseDto": {
+ "type": "object",
+ "properties": {
+ "tokens": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/StellarTokenDto"
+ }
+ }
+ },
+ "required": [
+ "tokens"
+ ]
+ },
+ "GuardianOverrideDto": {
+ "type": "object",
+ "properties": {}
+ },
+ "CreateSolverCredentialDto": {
+ "type": "object",
+ "properties": {
+ "scopes": {
+ "type": "string",
+ "enum": [
+ "solver:read",
+ "intents:read",
+ "quote:respond",
+ "intents:accept",
+ "intents:fill"
+ ],
+ "description": "Scopes granted to the credential"
+ },
+ "ipAllowlist": {
+ "description": "Optional source-address allowlist. Entries are an exact IPv4 address, an IPv4 CIDR block, an exact IPv6 address, or `*` for any source. IPv6 CIDR blocks and hostnames are not supported; an entry that does not parse never matches, so a typo locks the credential out rather than opening it up.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "expiresAt": {
+ "type": "number",
+ "description": "Unix epoch seconds at which the credential expires (null = never)"
+ }
+ },
+ "required": [
+ "scopes"
+ ]
+ },
+ "UpdateFlagDto": {
+ "type": "object",
+ "properties": {}
}
}
}
diff --git a/src/health/ws-gateway-health.indicator.ts b/src/health/ws-gateway-health.indicator.ts
index 5d2c1e72..7fe731c2 100644
--- a/src/health/ws-gateway-health.indicator.ts
+++ b/src/health/ws-gateway-health.indicator.ts
@@ -1,47 +1,29 @@
import { Injectable } from "@nestjs/common";
import { IntentsGateway } from "../intents/intents.gateway";
-import { HealthIndicator, HealthResult } from "./health-indicator.registry";
+import { HealthIndicator, IndicatorResult, ServiceRole } from "./health-indicator.registry";
/**
- * Health indicator for WebSocket gateway (Activity 2).
- *
+ * Health indicator for the WebSocket gateway (issue #511).
+ *
* Tracks draining state for graceful shutdown:
- * - During drain, readiness returns "not_ready" so load balancer stops routing
- * - Existing connections receive server_draining and close gradually
+ * - During drain, readiness returns "down" so the load balancer stops routing
+ * new connections to this replica
+ * - Existing connections receive `server_draining` and close gradually
*/
@Injectable()
export class WsGatewayHealthIndicator implements HealthIndicator {
- constructor(private readonly gateway: IntentsGateway) {}
-
- name(): string {
- return "ws_gateway";
- }
+ readonly name = "ws_gateway";
+ /** Critical for the `ws` role: a draining gateway must not receive traffic. */
+ readonly criticalFor: ServiceRole[] = ["ws"];
- critical(): boolean {
- return true; // Critical for "ws" role
- }
-
- async check(): Promise {
- const isDraining = this.gateway.isDraining();
+ constructor(private readonly gateway: IntentsGateway) {}
- if (isDraining) {
- return {
- status: "down",
- message: "WebSocket gateway is draining connections",
- details: {
- draining: true,
- subscribers: this.gateway.subscriberCount,
- },
- };
- }
+ async check(): Promise {
+ const draining = this.gateway.isDraining();
+ const subscribers = this.gateway.subscriberCount;
- return {
- status: "up",
- message: "WebSocket gateway accepting connections",
- details: {
- draining: false,
- subscribers: this.gateway.subscriberCount,
- },
- };
+ return draining
+ ? { status: "down", details: { draining: true, subscribers } }
+ : { status: "up", details: { draining: false, subscribers } };
}
}
diff --git a/src/intents/dto/list-intents.dto.ts b/src/intents/dto/list-intents.dto.ts
index dfab51e9..4906763d 100644
--- a/src/intents/dto/list-intents.dto.ts
+++ b/src/intents/dto/list-intents.dto.ts
@@ -1,91 +1,3 @@
-import { IsEnum, IsIn, IsInt, IsOptional, IsString, Max, MaxLength, Min } from "class-validator";
-import { Transform, Type } from "class-transformer";
-import { ApiPropertyOptional } from "@nestjs/swagger";
-import { IntentState } from "../intents.types";
-import { DEFAULT_PAGE_SIZE, MAX_OFFSET, MAX_PAGE_SIZE } from "../../common/pagination";
-
-/**
- * Query-parameter DTO for `GET /api/v1/intents` and
- * `GET /api/v1/intents/user/:addr` (#412).
- *
- * Cursor-based pagination replaces the old `offset` query param. The
- * `offset` param is still accepted but deprecated:
- * - Requests using `offset` receive a `Deprecation` response header.
- * - `offset` is hard-capped at MAX_OFFSET (10 000); requests above that
- * are rejected with HTTP 400.
- *
- * Stable ordering: `(createdAt DESC, intentId ASC)` — a tie-breaker is
- * needed because two intents can share the same createdAt second.
- */
-export class ListIntentsDto {
- /**
- * Filter by intent state. Omit to return all states.
- */
- @ApiPropertyOptional({
- enum: ["open", "accepted", "filled", "cancelled", "expired", "slashed"],
- description: "Return only intents in this state",
- })
- @IsOptional()
- @IsIn(["open", "accepted", "filled", "cancelled", "expired", "slashed"])
- state?: IntentState;
-
- /**
- * Maximum number of items to return (1–100, default 25).
- */
- @ApiPropertyOptional({
- type: Number,
- minimum: 1,
- maximum: MAX_PAGE_SIZE,
- default: DEFAULT_PAGE_SIZE,
- description: `Page size (1–${MAX_PAGE_SIZE}, default ${DEFAULT_PAGE_SIZE})`,
- })
- @IsOptional()
- @Type(() => Number)
- @IsInt()
- @Min(1)
- @Max(MAX_PAGE_SIZE)
- limit?: number;
-
- /**
- * Opaque keyset cursor returned by the previous page's `nextCursor`.
- * Mutually exclusive with `offset`.
- */
- @ApiPropertyOptional({
- type: String,
- description: "Opaque cursor from the previous page's `nextCursor` field",
- })
- @IsOptional()
- @IsString()
- @MaxLength(512)
- cursor?: string;
-
- /**
- * @deprecated Use `cursor` instead.
- *
- * Offset-based skip value. Capped at MAX_OFFSET; requests above that are
- * rejected with 400. A `Deprecation` header is included in every response
- * when this parameter is present.
- */
- @ApiPropertyOptional({
- type: Number,
- deprecated: true,
- description: `@deprecated — use cursor instead. Capped at ${MAX_OFFSET}.`,
- })
- @IsOptional()
- @Type(() => Number)
- @IsInt()
- @Min(0)
- @Max(MAX_OFFSET)
- offset?: number;
-
- /**
- * Filter by source chain.
- */
- @ApiPropertyOptional({ type: String, description: "Filter by source chain" })
- @IsOptional()
- @IsString()
- @MaxLength(32)
- chain?: string;
import { IsIn, IsInt, IsOptional, IsString, Max, Min } from "class-validator";
import { ApiPropertyOptional } from "@nestjs/swagger";
import {
diff --git a/src/intents/dual-write-intents.repository.spec.ts b/src/intents/dual-write-intents.repository.spec.ts
index 0b410460..8ad91bb5 100644
--- a/src/intents/dual-write-intents.repository.spec.ts
+++ b/src/intents/dual-write-intents.repository.spec.ts
@@ -21,7 +21,7 @@ class FakeSecondary extends InMemoryIntentsRepository {
async saveIfNewer(intent: Intent): Promise {
if (this.failWrites) throw new Error("postgres unavailable");
const current = this.findById(intent.intentId);
- if (!current || current.version < intent.version) this.save(intent);
+ if (!current || (current.version ?? 0) < (intent.version ?? 0)) this.save(intent);
}
override createIdempotent(intent: Intent, key: string, minCreatedAt: number) {
diff --git a/src/intents/dual-write-intents.repository.ts b/src/intents/dual-write-intents.repository.ts
index e12e7225..cc603763 100644
--- a/src/intents/dual-write-intents.repository.ts
+++ b/src/intents/dual-write-intents.repository.ts
@@ -47,7 +47,7 @@ export class DualWriteIntentsRepository implements IIntentsRepository {
let loadedFromPostgres = 0;
for (const intent of fromDb) {
const local = this.primary.findById(intent.intentId);
- if (!local || local.version < intent.version) {
+ if (!local || (local.version ?? 0) < (intent.version ?? 0)) {
this.primary.save(intent);
loadedFromPostgres++;
}
@@ -116,7 +116,7 @@ export class DualWriteIntentsRepository implements IIntentsRepository {
return this.primary.countActiveByUser(user);
}
- update(id: string, patch: IntentPatch, expectedVersion: number): Promise {
+ update(id: string, patch: IntentPatch, expectedVersion?: number): Promise {
return this.mirrored("update", this.primary.update(id, patch, expectedVersion));
}
diff --git a/src/intents/etag.ts b/src/intents/etag.ts
index 6defd215..d6ea2e01 100644
--- a/src/intents/etag.ts
+++ b/src/intents/etag.ts
@@ -10,7 +10,7 @@ import { VersionConflict } from "./intents.repository";
* endpoints; a mismatch yields `412 Precondition Failed` (RFC 9110 §13.1.1).
*/
export function etagFor(intent: Pick): string {
- return `"${intent.version}"`;
+ return `"${intent.version ?? 0}"`;
}
/**
diff --git a/src/intents/feed/intent-feed.service.ts b/src/intents/feed/intent-feed.service.ts
index 2b5c1a52..bbfaa7d0 100644
--- a/src/intents/feed/intent-feed.service.ts
+++ b/src/intents/feed/intent-feed.service.ts
@@ -3,6 +3,7 @@ import { ConfigService } from "@nestjs/config";
import { randomUUID } from "node:crypto";
import { IntentsService } from "../intents.service";
import { SolversService } from "../../solvers/solvers.service";
+import { SolverLivenessService } from "../../solvers/solver-liveness.service";
import { MetricsService } from "../../metrics/metrics.service";
import { logger } from "../../common/logger";
import { SUPPORTED_CHAINS, SupportedChain, IntentState } from "../intents.types";
@@ -71,6 +72,8 @@ export class IntentFeedService implements OnModuleDestroy {
@Optional() private readonly metricsService?: MetricsService,
@Optional() config?: ConfigService,
@Optional() @Inject(WS_BACKPLANE) backplane?: Backplane,
+ /** Solver liveness heartbeats (issue #445) — optional for older harnesses. */
+ @Optional() private readonly liveness?: SolverLivenessService,
) {
const defaults = configuration();
this.wsConfig = config?.get("ws", { infer: true }) ?? defaults.ws;
@@ -430,7 +433,11 @@ export class IntentFeedService implements OnModuleDestroy {
const solverRecord = await this.solversService.get(solverAddress);
if (!solverRecord) return;
- const predicate = buildMatchPredicate(solverRecord);
+ const liveness = this.liveness;
+ const predicate = buildMatchPredicate(
+ solverRecord,
+ liveness ? () => liveness.isLive(solverAddress) : undefined,
+ );
for (const [client, filter] of this.clients) {
if (filter.solver !== null && filter.solver.solverAddress === solverAddress) {
this.clients.set(client, { ...filter, solver: predicate });
diff --git a/src/intents/in-memory-intents.repository.spec.ts b/src/intents/in-memory-intents.repository.spec.ts
index 78870234..77332ccf 100644
--- a/src/intents/in-memory-intents.repository.spec.ts
+++ b/src/intents/in-memory-intents.repository.spec.ts
@@ -113,9 +113,10 @@ describe("InMemoryIntentsRepository", () => {
repo.save(makeIntent({ intentId: "upd-1", state: "open" }));
const updated = repo.update("upd-1", { state: "accepted", solver: "SOLVER_X" });
+ if (!updated || "kind" in updated) throw new Error("update should have succeeded");
- expect(updated?.state).toBe("accepted");
- expect(updated?.solver).toBe("SOLVER_X");
+ expect(updated.state).toBe("accepted");
+ expect(updated.solver).toBe("SOLVER_X");
expect(repo.findById("upd-1")?.state).toBe("accepted");
});
@@ -157,6 +158,4 @@ describe("InMemoryIntentsRepository", () => {
expect(repo.amendIfOpen("expired", { minDstAmount: "1", deadline: now + 200 }, now)).toBeNull();
expect(repo.amendIfOpen("missing", { minDstAmount: "1", deadline: now + 200 }, now)).toBeNull();
});
-});
- });
});
diff --git a/src/intents/intent-pending-states.spec.ts b/src/intents/intent-pending-states.spec.ts
index 764cc972..b7187d0a 100644
--- a/src/intents/intent-pending-states.spec.ts
+++ b/src/intents/intent-pending-states.spec.ts
@@ -19,6 +19,7 @@ import { Intent, IntentState, INTENT_STATES } from "./intents.types";
import { AppConfig } from "../config/configuration";
import { StellarTxService } from "../soroban/stellar-tx.service";
import { PrismaService } from "../prisma/prisma.service";
+import { ProtocolParamsService } from "../governance/params.service";
// ── helpers ────────────────────────────────────────────────────────────────
@@ -68,7 +69,18 @@ function makeService(
});
const stellarTx = opts.stellarTx ?? fakeStellarTx();
const prisma = fakePrisma();
- return new IntentsService(repo, config, stellarTx, prisma);
+ // Governance snapshot (issue #500): creation stamps `paramsVersion` from it,
+ // so every harness that creates intents must provide one.
+ const protocolParams = {
+ snapshotForChain: jest.fn().mockReturnValue({
+ version: 0,
+ feeBps: 30,
+ deadlineSeconds: 1800,
+ fillWindowSeconds: 600,
+ capturedAt: new Date().toISOString(),
+ }),
+ } as unknown as ProtocolParamsService;
+ return new IntentsService(repo, config, stellarTx, prisma, protocolParams);
}
function validCreateData(): Omit {
diff --git a/src/intents/intents-deadline.jobs.spec.ts b/src/intents/intents-deadline.jobs.spec.ts
index 13207613..07aef74d 100644
--- a/src/intents/intents-deadline.jobs.spec.ts
+++ b/src/intents/intents-deadline.jobs.spec.ts
@@ -47,6 +47,7 @@ function buildIntents(scheduler: IntentDeadlineScheduler): IntentsService {
undefined,
undefined,
undefined,
+ undefined,
scheduler,
);
}
@@ -66,6 +67,7 @@ describe("deadline jobs", () => {
intents,
{ broadcast: jest.fn().mockResolvedValue(undefined) } as unknown as IntentsGateway,
{} as SolversService,
+ null,
{ slashSolver: jest.fn().mockResolvedValue({ detail: "no-op" }) } as unknown as SolverRegistryService,
metrics,
{ evaluateTarget: jest.fn().mockReturnValue({ paused: false, matched: null, matchedChain: [] }) } as unknown as KillSwitchService,
diff --git a/src/intents/intents-sweeper.service.ts b/src/intents/intents-sweeper.service.ts
index 57f2b269..a94d8826 100644
--- a/src/intents/intents-sweeper.service.ts
+++ b/src/intents/intents-sweeper.service.ts
@@ -8,13 +8,18 @@ import { logger } from "../common/logger";
import { MetricsService } from "../metrics/metrics.service";
import { KillSwitchService } from "../killswitch/killswitch.service";
import { Intent } from "./intents.types";
+import { DeadlineJobData, DEADLINE_QUEUE, EXPIRE_INTENT_JOB, FILL_WINDOW_EXPIRED_JOB } from "./intents-deadline.jobs";
+import { JobsService } from "../jobs/jobs.service";
import {
+ AppConfig,
CHAIN_FILL_WINDOW_DEFAULTS,
DEFAULT_FILL_WINDOW_SECONDS,
} from "../config/configuration";
import { LeaderElectionService, Singleton } from "../common/leader-election";
+import { ConfigService } from "@nestjs/config";
-const SWEEP_INTERVAL_MS = 30_000;
+/** Low-frequency safety scan. Deadline jobs are the primary expiry path (issue #437). */
+const SAFETY_SWEEP_INTERVAL_MS = 300_000;
/** Outcome of a single sweep cycle — returned so a manual trigger can log it. */
export interface SweepResult {
@@ -40,9 +45,14 @@ export class IntentsSweeperService implements OnModuleInit, OnModuleDestroy {
private readonly metricsService: MetricsService,
private readonly killSwitch: KillSwitchService,
private readonly leaderElection: LeaderElectionService,
+ @Optional() private readonly config?: ConfigService,
+ @Optional() private readonly jobs?: JobsService,
) {}
onModuleInit() {
+ this.jobs?.defineQueue(DEADLINE_QUEUE, { concurrency: 8 });
+ this.jobs?.process(EXPIRE_INTENT_JOB, (data) => this.handleExpireJob(data));
+ this.jobs?.process(FILL_WINDOW_EXPIRED_JOB, (data) => this.handleFillWindowJob(data));
this.leaderElection.registerWorker("sweeper", (isLeader, _token) => {
if (isLeader) {
this.logger.log("[sweeper] became leader — starting interval");
@@ -61,10 +71,10 @@ export class IntentsSweeperService implements OnModuleInit, OnModuleDestroy {
private startInterval(): void {
if (this.interval) return; // already running
this.interval = setInterval(() => {
- this.sweep().catch((err) => {
+ this.sweep({ safety: true }).catch((err) => {
logger.error(`[sweeper] sweep failed: ${err instanceof Error ? err.message : err}`);
});
- }, SWEEP_INTERVAL_MS);
+ }, this.safetyIntervalMs());
}
private stopInterval(): void {
@@ -74,7 +84,7 @@ export class IntentsSweeperService implements OnModuleInit, OnModuleDestroy {
}
}
- async sweep(): Promise {
+ async sweep(options?: { safety?: boolean }): Promise {
const startMs = Date.now();
const now = Math.floor(startMs / 1000);
let expiredCount = 0;
@@ -82,22 +92,7 @@ export class IntentsSweeperService implements OnModuleInit, OnModuleDestroy {
let extendedDeadlines = 0;
for (const intent of await this.intentsService.getByState("open")) {
- if (intent.deadline <= now) {
- // Atomic guard: a concurrent user cancel() or solver accept() may have
- // already transitioned this intent out of "open" — skip it if so.
- const expired = await this.intentsService.expireIfOpen(intent.intentId);
- if (!expired) continue;
- // Audit trail (issue #62): system-driven expiration.
- this.intentsService.appendAuditEntry(
- intent.intentId,
- "expired",
- "system",
- "deadline passed",
- { deadline: intent.deadline, sweepedAt: now },
- );
- expiredCount++;
- await this.intentsGateway.broadcast({ type: "intent_expired", intentId: intent.intentId });
- }
+ if (intent.deadline <= now && (await this.expireOpen(intent, now))) expiredCount++;
}
const durationMs = Date.now() - startMs;
@@ -122,26 +117,13 @@ export class IntentsSweeperService implements OnModuleInit, OnModuleDestroy {
// its window instead of slashing; the intent becomes fillable again on
// resume. Evaluated per intent because a pause may be scoped to a single
// chain or token.
- const deadline = this.pausedFillDeadline(intent, now);
- if (deadline !== null) {
- const extended = await this.intentsService.extendDeadlineIfAccepted(
- intent.intentId,
- deadline,
- );
- if (extended) {
- extendedDeadlines++;
- this.logger.warn(
- `[sweeper] intent ${intent.intentId} fill is paused by a kill-switch — ` +
- `slashing suppressed and deadline extended to ${deadline}`,
- );
- }
- continue;
- }
-
- const slashed = await this.slashMissedFill(intent.intentId, intent.solver, now);
- if (slashed) slashedCount++;
+ const outcome = await this.settleAccepted(intent, now);
+ if (outcome === "slashed") slashedCount++;
+ if (outcome === "extended") extendedDeadlines++;
}
+ if (options?.safety) this.metricsService.recordSafetyCatch?.(expiredCount + slashedCount);
+
return {
expiredCount,
slashedCount,
@@ -169,6 +151,65 @@ export class IntentsSweeperService implements OnModuleInit, OnModuleDestroy {
return now + window;
}
+ /**
+ * `expire-intent` handler. No-ops when the intent is no longer open or the
+ * stored deadline is not the one this job was armed for (amendment / stale job).
+ */
+ async handleExpireJob(data: DeadlineJobData): Promise {
+ const intent = await this.intentsService.get(data.intentId);
+ if (!intent || intent.state !== "open" || intent.deadline !== data.deadline) return;
+ const now = Math.floor(Date.now() / 1000);
+ if (intent.deadline > now) return;
+ await this.expireOpen(intent, now);
+ }
+
+ /**
+ * `fill-window-expired` handler. No-ops on a terminal state or a deadline
+ * that has since been moved. A kill-switch pause extends the window instead
+ * of slashing, and that extension arms a new job.
+ */
+ async handleFillWindowJob(data: DeadlineJobData): Promise {
+ const intent = await this.intentsService.get(data.intentId);
+ if (!intent || intent.state !== "accepted" || intent.deadline !== data.deadline) return;
+ const now = Math.floor(Date.now() / 1000);
+ if (intent.deadline > now) return;
+ await this.settleAccepted(intent, now);
+ }
+
+ private safetyIntervalMs(): number {
+ const configured = this.config?.get("safetySweepIntervalMs", { infer: true });
+ return configured && configured > 0 ? configured : SAFETY_SWEEP_INTERVAL_MS;
+ }
+
+ private async expireOpen(intent: Intent, now: number): Promise {
+ const expired = await this.intentsService.expireIfOpen(intent.intentId);
+ if (!expired) return false;
+ this.intentsService.appendAuditEntry(
+ intent.intentId,
+ "expired",
+ "system",
+ "deadline passed",
+ { deadline: intent.deadline, sweepedAt: now },
+ );
+ await this.intentsGateway.broadcast({ type: "intent_expired", intentId: intent.intentId });
+ return true;
+ }
+
+ private async settleAccepted(intent: Intent, now: number): Promise<"slashed" | "extended" | "skipped"> {
+ const deadline = this.pausedFillDeadline(intent, now);
+ if (deadline !== null) {
+ const extended = await this.intentsService.extendDeadlineIfAccepted(intent.intentId, deadline);
+ if (!extended) return "skipped";
+ this.logger.warn(
+ `[sweeper] intent ${intent.intentId} fill is paused by a kill-switch — ` +
+ `slashing suppressed and deadline extended to ${deadline}`,
+ );
+ return "extended";
+ }
+ const slashed = await this.slashMissedFill(intent.intentId, intent.solver, now);
+ return slashed ? "slashed" : "skipped";
+ }
+
/**
* Issue #269 — safe, auditable manual sweep trigger (operator break-glass).
*
diff --git a/src/intents/intents.controller.ts b/src/intents/intents.controller.ts
index a915fb49..5f5e3142 100644
--- a/src/intents/intents.controller.ts
+++ b/src/intents/intents.controller.ts
@@ -1,260 +1,16 @@
import {
BadRequestException,
Body,
- Controller,
- Get,
- HttpCode,
- NotFoundException,
- Param,
- Post,
- Query,
- Res,
-} from "@nestjs/common";
-import { ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags } from "@nestjs/swagger";
-import type { Response } from "express";
-import { IntentsService } from "./intents.service";
-import { BatchLookupDto } from "./dto/batch-lookup.dto";
-import { AcceptIntentDto } from "./dto/accept-intent.dto";
-import { ListIntentsDto } from "./dto/list-intents.dto";
-import { Intent } from "./intents.types";
-import {
- PaginatedResponse,
- encodeCursor,
- decodeCursor,
- hashFilter,
- getCursorSecret,
- DEFAULT_PAGE_SIZE,
- MAX_OFFSET,
-} from "../common/pagination";
-
-/**
- * REST controller for intent lifecycle (#412 pagination, #410 FK columns).
- *
- * All list endpoints use keyset pagination. Legacy `offset` params are still
- * accepted but deprecated: callers receive a `Deprecation` response header.
- */
-@ApiTags("intents")
-@Controller("api/v1/intents")
-export class IntentsController {
- constructor(private readonly intentsService: IntentsService) {}
-
- // ─── List intents ─────────────────────────────────────────────────────────
-
- @Get()
- @ApiOperation({ summary: "List intents (keyset-paginated)" })
- async list(
- @Query() query: ListIntentsDto,
- @Res({ passthrough: true }) res: Response,
- ): Promise> {
- return this.listPage(query, res);
- }
-
- // ─── Get by user ──────────────────────────────────────────────────────────
-
- @Get("user/:addr")
- @ApiOperation({ summary: "List intents for a user (keyset-paginated)" })
- @ApiParam({ name: "addr", description: "User Stellar or EVM address" })
- async listByUser(
- @Param("addr") addr: string,
- @Query() query: ListIntentsDto,
- @Res({ passthrough: true }) res: Response,
- ): Promise> {
- return this.listPage({ ...query, user: addr }, res);
- }
-
- // ─── Get single intent ────────────────────────────────────────────────────
-
- @Get(":id")
- @ApiOperation({ summary: "Get intent by ID" })
- async getById(@Param("id") id: string): Promise {
- const intent = await this.intentsService.get(id);
- if (!intent) throw new NotFoundException(`Intent ${id} not found`);
- return intent;
- }
-
- // ─── Batch lookup ─────────────────────────────────────────────────────────
-
- @Post("batch")
- @HttpCode(200)
- @ApiOperation({ summary: "Batch-fetch intents by IDs" })
- async batchLookup(@Body() dto: BatchLookupDto): Promise {
- return this.intentsService.getMany(dto.intentIds);
- }
-
- // ─── Audit log ────────────────────────────────────────────────────────────
-
- /**
- * GET /api/v1/intents/:id/audit — keyset-paginated audit log (#412).
- *
- * Returns audit entries for the given intent in newest-first order.
- * The `offset` param is deprecated; use `cursor` instead.
- */
- @Get(":id/audit")
- @ApiOperation({ summary: "Intent audit log (keyset-paginated)" })
- @ApiQuery({ name: "limit", required: false, type: Number })
- @ApiQuery({ name: "cursor", required: false, type: String })
- @ApiQuery({ name: "offset", required: false, type: Number, deprecated: true })
- async getAuditLog(
- @Param("id") id: string,
- @Query("limit") rawLimit?: string,
- @Query("cursor") cursor?: string,
- @Query("offset") rawOffset?: string,
- @Res({ passthrough: true }) res?: Response,
- ): Promise> {
- const intent = await this.intentsService.get(id);
- if (!intent) throw new NotFoundException(`Intent ${id} not found`);
-
- const limit = Math.min(parseInt(rawLimit ?? String(DEFAULT_PAGE_SIZE), 10) || DEFAULT_PAGE_SIZE, 100);
- const offset = rawOffset !== undefined ? parseInt(rawOffset, 10) : undefined;
-
- if (offset !== undefined && !Number.isNaN(offset)) {
- if (offset > MAX_OFFSET) {
- throw new BadRequestException(`offset exceeds maximum of ${MAX_OFFSET}; use cursor pagination`);
- }
- res?.setHeader("Deprecation", "true");
- res?.setHeader("Link", `; rel="successor-version"`);
- }
-
- const allEntries = this.intentsService.getAuditLog(id);
- const skip = cursor
- ? this.auditCursorToOffset(cursor, id)
- : (offset ?? 0);
- const page = allEntries.slice(skip, skip + limit);
- const hasMore = skip + limit < allEntries.length;
- const nextCursor = hasMore
- ? this.encodeAuditCursor(skip + limit, id)
- : null;
-
- return new PaginatedResponse(page, nextCursor);
- }
-
- // ─── Accept / Fill / Cancel ───────────────────────────────────────────────
-
- @Post(":id/accept")
- @HttpCode(200)
- @ApiOperation({ summary: "Accept an intent" })
- @ApiHeader({ name: "X-Idempotency-Key", required: false })
- async accept(
- @Param("id") id: string,
- @Body() dto: AcceptIntentDto,
- ): Promise {
- const updated = await this.intentsService.acceptIfOpen(id, dto.solver);
- if (!updated) throw new BadRequestException("Intent is not open or past deadline");
- return updated;
- }
-
- @Post(":id/cancel")
- @HttpCode(200)
- @ApiOperation({ summary: "Cancel an open intent" })
- async cancel(@Param("id") id: string): Promise {
- const updated = await this.intentsService.cancelIfOpen(id);
- if (!updated) throw new BadRequestException("Intent is not open");
- return updated;
- }
-
- // ─── Private helpers ──────────────────────────────────────────────────────
-
- /**
- * Core list logic shared by GET /intents and GET /intents/user/:addr.
- */
- private async listPage(
- query: ListIntentsDto & { user?: string },
- res: Response,
- ): Promise> {
- const limit = Math.min(query.limit ?? DEFAULT_PAGE_SIZE, 100);
- const secret = getCursorSecret();
-
- // Build a filter fingerprint to bind the cursor.
- const filterObj: Record = {};
- if (query.state) filterObj.state = query.state;
- if (query.user) filterObj.user = query.user;
- if (query.chain) filterObj.chain = query.chain;
- const filterHash = hashFilter(filterObj);
-
- // Deprecated offset fallback.
- let offset: number | undefined;
- if (query.offset !== undefined) {
- if (query.offset > MAX_OFFSET) {
- throw new BadRequestException(`offset exceeds maximum of ${MAX_OFFSET}; use cursor pagination`);
- }
- res.setHeader("Deprecation", "true");
- res.setHeader("Link", "; rel=\"successor-version\"");
- offset = query.offset;
- }
-
- // Decode cursor position.
- let cursorPayload: { createdAt: number; id: string } | undefined;
- if (query.cursor) {
- cursorPayload = decodeCursor(query.cursor, secret, filterHash);
- }
-
- // Fetch all matching intents (sorted createdAt DESC, intentId ASC).
- let all: Intent[];
- if (query.state) {
- all = await this.intentsService.getByState(query.state);
- } else if (query.user) {
- all = await this.intentsService.getByUser(query.user);
- } else {
- all = await this.intentsService.getAll();
- }
-
- // Apply chain filter in-memory (fast path for in-memory adapter).
- if (query.chain) {
- all = all.filter((i) => i.srcChain === query.chain);
- }
-
- // Sort: createdAt DESC, intentId ASC (stable tie-breaker).
- all.sort((a, b) => {
- if (b.createdAt !== a.createdAt) return b.createdAt - a.createdAt;
- return a.intentId.localeCompare(b.intentId);
- });
-
- // Seek to cursor position.
- let startIdx = offset ?? 0;
- if (cursorPayload) {
- const pos = all.findIndex(
- (i) => i.createdAt < cursorPayload!.createdAt ||
- (i.createdAt === cursorPayload!.createdAt && i.intentId > cursorPayload!.id),
- );
- startIdx = pos === -1 ? all.length : pos;
- }
-
- const page = all.slice(startIdx, startIdx + limit);
- const hasMore = startIdx + limit < all.length;
-
- let nextCursor: string | null = null;
- if (hasMore && page.length > 0) {
- const last = page[page.length - 1];
- nextCursor = encodeCursor({ createdAt: last.createdAt, id: last.intentId, filterHash }, secret);
- }
-
- return new PaginatedResponse(page, nextCursor);
- }
-
- /**
- * Encode an audit-log offset as an opaque cursor.
- * The cursor is intentId-scoped so it cannot be used against a different intent.
- */
- private encodeAuditCursor(offset: number, intentId: string): string {
- const secret = getCursorSecret();
- return encodeCursor({ createdAt: offset, id: intentId, filterHash: hashFilter({ intentId }) }, secret);
- }
-
- private auditCursorToOffset(cursor: string, intentId: string): number {
- const secret = getCursorSecret();
- const filterHash = hashFilter({ intentId });
- const payload = decodeCursor(cursor, secret, filterHash);
- return payload.createdAt; // createdAt field holds the offset for audit log cursors
ConflictException,
Controller,
ForbiddenException,
Get,
GoneException,
NotFoundException,
- Optional,
Param,
Post,
Query,
+ UnprocessableEntityException,
UseGuards,
} from "@nestjs/common";
import {
@@ -268,28 +24,35 @@ import {
ApiTooManyRequestsResponse,
ApiOperation,
ApiServiceUnavailableResponse,
+ ApiUnprocessableEntityResponse,
} from "@nestjs/swagger";
import { Throttle } from "@nestjs/throttler";
import { IntentsService } from "./intents.service";
import { IntentsGateway } from "./intents.gateway";
import { SolversService } from "../solvers/solvers.service";
-import { SolverGriefingService } from "../solvers/solver-griefing.service";
import { TokensService } from "../tokens/tokens.service";
import { RoutingService } from "../routing/routing.service";
-import { MAX_OPEN_INTENTS_PER_USER } from "./intents.service";
+import { MAX_OPEN_INTENTS_PER_USER, NewIntentData } from "./intents.service";
import { CreateIntentDto } from "./dto/create-intent.dto";
import { CHAIN_DEADLINE_DEFAULTS, DEFAULT_DEADLINE_SECONDS } from "../config/configuration";
import { AcceptIntentDto } from "./dto/accept-intent.dto";
import { FillIntentDto } from "./dto/fill-intent.dto";
import { CancelIntentDto } from "./dto/cancel-intent.dto";
+import { AmendIntentDto } from "./dto/amend-intent.dto";
import { QuoteRequestDto } from "./dto/quote-request.dto";
import { QuoteResponseDto } from "./dto/quote-response.dto";
import { ListIntentsDto } from "./dto/list-intents.dto";
import { BatchLookupDto } from "./dto/batch-lookup.dto";
+import {
+ BatchCreateIntentsDto,
+ BatchCreateIntentsResponseDto,
+} from "./dto/batch-create-intents.dto";
+import { BATCH_CREATE_MAX_INTENTS } from "../config/limits.config";
import { UserThrottlerGuard } from "./user-throttler.guard";
import {
verifyStellarSignature,
buildAcceptMessage,
+ buildAmendMessage,
buildCancelMessage,
buildFillMessage,
} from "../common/stellar-signature";
@@ -313,12 +76,11 @@ import { AppConfig } from "../config/configuration";
import { isCanaryIntent } from "../common/canary";
@ApiTags("intents")
-@Controller("api/v1/intents")
+@Controller({ path: "intents", version: "1" })
export class IntentsController {
constructor(
private readonly intentsService: IntentsService,
private readonly solversService: SolversService,
- @Optional() private readonly griefingService: SolverGriefingService | null,
private readonly intentsGateway: IntentsGateway,
private readonly tokensService: TokensService,
private readonly routingService: RoutingService,
@@ -575,6 +337,88 @@ export class IntentsController {
return { intents, count: intents.length };
}
+ /**
+ * POST /api/v1/intents/batch-create
+ *
+ * Issue #429 — Atomic batch intent creation endpoint.
+ * Creates up to N intents atomically (all-or-nothing) with per-item validation errors.
+ */
+ @Post("batch-create")
+ @UseGuards(UserThrottlerGuard, KillSwitchGuard)
+ @KillSwitchGate({ operation: "create" })
+ @ApiOperation({
+ summary: "Create multiple intents atomically",
+ description:
+ "Creates up to 50 intents in a single atomic (all-or-nothing) request. " +
+ "If any item fails validation or exceeds open intent limits, zero intents are created " +
+ "and per-item errors are reported.",
+ })
+ @ApiOkResponse({ type: BatchCreateIntentsResponseDto })
+ @ApiBadRequestResponse({ description: "Invalid request payload or empty batch" })
+ @ApiUnprocessableEntityResponse({ description: "Per-item validation errors or limits exceeded" })
+ async batchCreate(@Body() dto: BatchCreateIntentsDto) {
+ if (!dto.intents || !Array.isArray(dto.intents) || dto.intents.length === 0) {
+ throw new BadRequestException("Intents array must contain at least 1 item");
+ }
+
+ if (dto.intents.length > BATCH_CREATE_MAX_INTENTS) {
+ throw new BadRequestException(
+ `Batch size exceeds maximum allowed limit of ${BATCH_CREATE_MAX_INTENTS}`,
+ );
+ }
+
+ const now = Math.floor(Date.now() / 1000);
+ const items: NewIntentData[] = [];
+
+ for (let i = 0; i < dto.intents.length; i++) {
+ const itemDto = dto.intents[i];
+ const srcToken = await this.tokensService.resolveSrcTokenOrThrow(
+ itemDto.srcChain as SupportedChain,
+ itemDto.srcTokenAddress,
+ );
+ const dstToken = await this.tokensService.resolveDstTokenOrThrow(itemDto.dstTokenContract);
+
+ items.push({
+ user: itemDto.user,
+ srcChain: itemDto.srcChain,
+ srcToken: {
+ address: itemDto.srcTokenAddress,
+ symbol: itemDto.srcTokenSymbol,
+ name: itemDto.srcTokenSymbol,
+ decimals: itemDto.srcTokenDecimals,
+ chain: itemDto.srcChain,
+ priceUSD: srcToken?.priceUSD,
+ },
+ srcAmount: itemDto.srcAmount,
+ dstToken: {
+ contract: itemDto.dstTokenContract,
+ symbol: itemDto.dstTokenSymbol,
+ decimals: itemDto.dstTokenDecimals,
+ priceUSD: dstToken?.priceUSD,
+ },
+ minDstAmount: itemDto.minDstAmount,
+ deadline: itemDto.deadline ?? now + (CHAIN_DEADLINE_DEFAULTS[itemDto.srcChain] ?? DEFAULT_DEADLINE_SECONDS),
+ });
+ }
+
+ const result = await this.intentsService.createBatch(items);
+
+ if (result.errors.length > 0) {
+ throw new UnprocessableEntityException({
+ statusCode: 422,
+ message: "Batch intent creation failed validation",
+ created: [],
+ errors: result.errors,
+ });
+ }
+
+ for (const intent of result.created) {
+ this.intentsGateway.broadcast({ type: "intent_created", intent });
+ }
+
+ return { created: result.created, errors: [] };
+ }
+
@Post(":id/accept")
@UseGuards(KillSwitchGuard)
@KillSwitchGate({ operation: "accept" })
@@ -616,16 +460,6 @@ export class IntentsController {
if (this.solversService.isSuspended(dto.solver)) {
throw new ForbiddenException("Solver is suspended by an active guardian action");
}
- // Anti-griefing enforcement (issue #453): check rolling unfilled-accept
- // ratio before allowing the accept. Canary solvers are exempt — their
- // fills are synthetic and must not inflate the enforcement counters.
- if (!this.canary.has(dto.solver) && this.griefingService) {
- const currentOpenAccepts = await this.intentsService.getAcceptedCountBySolver(dto.solver);
- const griefingCheck = this.griefingService.checkAcceptAllowed(dto.solver, currentOpenAccepts, now);
- if (!griefingCheck.allowed) {
- throw new ForbiddenException(griefingCheck.reason ?? "Solver is blocked by anti-griefing controls");
- }
- }
// Canary intents pair only with canary solvers (issue #496) so synthetic
// traffic never affects real solvers' stats or real users' fills.
if (isCanaryIntent(intent, this.canary) !== this.canary.has(dto.solver)) {
@@ -645,10 +479,6 @@ export class IntentsController {
this.intentsService.appendAuditEntry(id, "accepted", dto.solver, "solver accepted", {
deadline: updated.deadline,
});
- // Anti-griefing: record the accept in the rolling window (issue #453).
- if (!this.canary.has(dto.solver) && this.griefingService) {
- this.griefingService.recordAccept(dto.solver, id, now);
- }
this.intentsGateway.broadcast({
type: "intent_accepted",
intentId: id,
@@ -764,6 +594,55 @@ export class IntentsController {
return updated;
}
+ /**
+ * Issue #569 — amend the terms of an open intent.
+ *
+ * Replaces `minDstAmount` and `deadline` on an intent that is still `open`
+ * and whose deadline has not passed. Both replacement values are covered by
+ * the creator's Ed25519 signature (see `buildAmendMessage`), so neither can
+ * change without the owner's consent. The write itself goes through
+ * `amendIfOpen`, whose state + deadline predicates make it race-free against
+ * a concurrent accept, and it deliberately does not bump `version` —
+ * widening the user's terms does not compete with solver writes.
+ */
+ @Post(":id/amend")
+ @ApiNotFoundResponse({ description: "Intent not found" })
+ @ApiForbiddenResponse({ description: "Unauthorized" })
+ @ApiConflictResponse({ description: "Intent is not amendable" })
+ async amend(@Param("id") id: string, @Body() dto: AmendIntentDto): Promise {
+ const intent = await this.intentsService.get(id);
+ if (!intent) throw new NotFoundException("Intent not found");
+ if (intent.user.toLowerCase() !== dto.user.toLowerCase()) {
+ throw new ForbiddenException("Unauthorized");
+ }
+
+ // The signature must cover the replacement values themselves, so a
+ // tampered minDstAmount or deadline fails verification before any write.
+ verifyStellarSignature(
+ dto.user,
+ buildAmendMessage(id, dto.user, dto.minDstAmount, dto.deadline),
+ dto.signature,
+ );
+
+ const amended = await this.intentsService.amendIfOpen(id, {
+ minDstAmount: dto.minDstAmount,
+ deadline: dto.deadline,
+ });
+ if (!amended) {
+ const current = await this.intentsService.get(id);
+ throw new ConflictException(`Cannot amend intent in state: ${current?.state ?? "unknown"}`);
+ }
+
+ this.intentsService.appendAuditEntry(id, "open", dto.user, "user amended", {
+ previousMinDstAmount: intent.minDstAmount,
+ minDstAmount: dto.minDstAmount,
+ previousDeadline: intent.deadline,
+ deadline: dto.deadline,
+ });
+
+ return amended;
+ }
+
/**
* Issue #44 — document 429 on quote too, since it's under the global guard.
* Issue #220 — routes are now computed via RoutingService and attached to each quote.
diff --git a/src/intents/intents.gateway.spec.ts b/src/intents/intents.gateway.spec.ts
index ca66ef85..54d144ff 100644
--- a/src/intents/intents.gateway.spec.ts
+++ b/src/intents/intents.gateway.spec.ts
@@ -214,7 +214,11 @@ describe("IntentsGateway heartbeat", () => {
const signature = keypair.sign(Buffer.from(message, "utf8")).toString("base64");
await client._listeners.message(JSON.stringify({ type: "auth", solver: keypair.publicKey(), timestamp, signature }));
- expect(client.send).toHaveBeenLastCalledWith(JSON.stringify({ type: "auth_ok" }));
+ // Ack the auth first, then immediately push the scoped eligibility
+ // snapshot (#436) — so the snapshot, not the ack, is the final frame of
+ // a successful handshake.
+ expect(client.send).toHaveBeenCalledWith(JSON.stringify({ type: "auth_ok" }));
+ expect(client.send).toHaveBeenLastCalledWith(expect.stringContaining('"type":"eligible_snapshot"'));
await client._listeners.message(JSON.stringify({ type: "auth", solver: keypair.publicKey(), timestamp, signature: "bad" }));
expect(client.send).toHaveBeenLastCalledWith(JSON.stringify({ type: "auth_error", reason: "invalid solver signature" }));
diff --git a/src/intents/intents.gateway.ts b/src/intents/intents.gateway.ts
index 7ebd6adf..c4e7d399 100644
--- a/src/intents/intents.gateway.ts
+++ b/src/intents/intents.gateway.ts
@@ -1,55 +1,48 @@
-import { Injectable, Logger, Optional } from "@nestjs/common";
-import { ConfigService } from "@nestjs/config";
-import { AppConfig } from "../config/configuration";
-
-/**
- * IntentsGateway — WebSocket gateway for real-time intent events.
- *
- * Stub that exposes `getSubscriberCount()` used by StatsService.
- * Full WebSocket implementation delegates to IntentFeedService (issue #433).
- *
- * The stub is Injectable so it can be provided in test modules without
- * requiring a real WS server.
- */
-@Injectable()
-export class IntentsGateway {
- private readonly logger = new Logger(IntentsGateway.name);
-
- constructor(
- @Optional() private readonly config?: ConfigService,
- ) {}
-
- /** Returns the number of currently-connected WebSocket subscribers. */
- getSubscriberCount(): number {
- // Delegates to the feed service in the real implementation.
- // Returning 0 here is correct for the stub / test path.
- return 0;
-import { OnModuleDestroy, Optional, Inject } from "@nestjs/common";
+import { Inject, OnModuleDestroy, Optional } from "@nestjs/common";
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway } from "@nestjs/websockets";
import { WebSocket } from "ws";
import { IntentsService } from "./intents.service";
import { SolversService } from "../solvers/solvers.service";
+import { SolverLivenessService } from "../solvers/solver-liveness.service";
import { MetricsService } from "../metrics/metrics.service";
import { logger } from "../common/logger";
import { SUPPORTED_CHAINS, SupportedChain } from "./intents.types";
import { verifyStellarSignature, buildWsAuthMessage } from "../common/stellar-signature";
import { buildMatchPredicate, IntentCapabilityIndex, SolverMatchPredicate } from "./solver-intent-matcher";
-import {
- WS_MAX_FILTER_CHAINS,
- WS_MAX_SUBSCRIPTIONS_PER_CONNECTION,
-} from "../config/limits.config";
+import { IntentFeedService } from "./feed/intent-feed.service";
+import type { BackplaneHealth } from "./backplane/backplane.types";
+import type { ReplayStore } from "./backplane/replay-store";
+import { REPLAY_STORE } from "./backplane/replay-store.token";
import {
negotiateProtocol,
resolveProtocol,
- WS_CLOSE_UNSUPPORTED_PROTOCOL,
WS_CLOSE_REASON_UNSUPPORTED,
+ WS_CLOSE_UNSUPPORTED_PROTOCOL,
} from "./ws-protocol";
-import { REPLAY_STORE } from "./backplane/replay-store.token";
-import { ReplayStore, SequencedEvent } from "./backplane/replay-store";
-import { MemoryReplayStore } from "./backplane/memory-replay.store";
+import {
+ WS_MAX_FILTER_CHAINS,
+ WS_MAX_SUBSCRIPTIONS_PER_CONNECTION,
+} from "../config/limits.config";
const HEARTBEAT_INTERVAL_MS = 30_000;
+/**
+ * How many sequenced events to keep in the replay buffer.
+ *
+ * At typical broadcast volume (a few dozen events/minute in production),
+ * 500 events covers many minutes of missed events — more than enough to
+ * bridge a transient network blip or container restart without forcing a
+ * full snapshot re-fetch. Increasing this beyond ~1 000 starts to add
+ * non-trivial heap pressure for large event payloads; the current bound
+ * is a deliberate memory vs. reconnect-gap tradeoff.
+ */
+const REPLAY_BUFFER_SIZE = 500;
+
+export interface SequencedEvent {
+ seq: number;
+ type: string;
+ [key: string]: unknown;
+}
/**
* Per-subscriber filter (issue #436).
@@ -73,6 +66,49 @@ interface SubscriberFilter {
subscriptionCount: number;
}
+/**
+ * Fixed-size ring buffer that retains the last `capacity` events so
+ * reconnecting clients can request a replay from a known sequence number.
+ */
+export class EventRingBuffer {
+ private readonly buf: SequencedEvent[] = [];
+ private readonly capacity: number;
+
+ constructor(capacity = REPLAY_BUFFER_SIZE) {
+ this.capacity = capacity;
+ }
+
+ push(event: SequencedEvent): void {
+ if (this.buf.length >= this.capacity) {
+ this.buf.shift();
+ }
+ this.buf.push(event);
+ }
+
+ /**
+ * Return all buffered events whose seq is strictly greater than `fromSeq`.
+ * Returns an empty array when `fromSeq` is older than the earliest buffered
+ * event (the caller should request a fresh snapshot instead).
+ */
+ since(fromSeq: number): SequencedEvent[] {
+ return this.buf.filter((e) => e.seq > fromSeq);
+ }
+
+ /** Lowest seq still in the buffer, or -1 when empty. */
+ oldestSeq(): number {
+ return this.buf.length === 0 ? -1 : this.buf[0].seq;
+ }
+
+ /** Highest seq in the buffer, or 0 when empty. */
+ latestSeq(): number {
+ return this.buf.length === 0 ? 0 : this.buf[this.buf.length - 1].seq;
+ }
+
+ size(): number {
+ return this.buf.length;
+ }
+}
+
/**
* Authentication / access-control decision (issue #49, updated #436)
* ─────────────────────────────────────────────────────────────────────
@@ -100,8 +136,9 @@ interface SubscriberFilter {
* versions — giving the client a descriptive WS reason string.
*
* - vortex.v1 offered → echo "vortex.v1"
- * - no protocol offered → echo "vortex.v1" (backward-compatible default)
- * - unknown protocol → echo "" (empty); handleConnection closes 1002
+ * - no protocol offered → echo "" (resolveProtocol treats "" as vortex.v1)
+ * - unknown protocol → echo the first offered token; handleConnection
+ * then closes with 1002
*/
handleProtocols: negotiateProtocol,
})
@@ -117,22 +154,46 @@ export class IntentsGateway
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private heartbeatTimer: any;
private nextSeq = 1;
+ /** True once graceful connection draining has begun (issue #511). */
+ private draining = false;
private readonly backplane: null | {
publish: (event: Record) => void;
subscribe: (handler: (event: Record) => void) => void;
} = null;
/** Ring buffer storing the last REPLAY_BUFFER_SIZE broadcast events. */
- private replayStore: ReplayStore;
+ private readonly ringBuffer = new EventRingBuffer(REPLAY_BUFFER_SIZE);
+
+ /**
+ * Sequenced replay store (issue #457).
+ *
+ * Provided by the IntentsModule via the REPLAY_STORE token (memory or
+ * Redis, selected by WS_REPLAY_STORE). When absent — direct unit-harness
+ * construction — replay falls back to the in-process ring buffer above,
+ * which the broadcast path always keeps populated as well.
+ */
+ private readonly replayStore: ReplayStore | null;
constructor(
private readonly intentsService: IntentsService,
private readonly solversService: SolversService,
private readonly intentIndex: IntentCapabilityIndex,
@Optional() private readonly metricsService?: MetricsService,
- @Optional() @Inject(REPLAY_STORE) replayStoreParam: ReplayStore | null = null,
+ /**
+ * Sequenced replay store (issue #457) — 5th positional parameter so the
+ * backplane test harnesses can inject a MemoryReplayStore directly.
+ */
+ @Optional() @Inject(REPLAY_STORE) replayStoreParam?: ReplayStore | null,
+ /**
+ * SSE intent feed (issues #454, #492). Injected `@Optional()` so graphs
+ * that do not mount the feed — and the unit harnesses that construct this
+ * gateway directly — still run; it only supplies backplane health detail.
+ */
+ @Optional() private readonly feed?: IntentFeedService,
+ /** Solver liveness heartbeats (issue #445) — optional for older harnesses. */
+ @Optional() private readonly liveness?: SolverLivenessService,
) {
- this.replayStore = replayStoreParam ?? new MemoryReplayStore({ maxCount: 500 });
+ this.replayStore = replayStoreParam ?? null;
this.heartbeatTimer = setInterval(() => this.heartbeat(), HEARTBEAT_INTERVAL_MS);
this.backplane = this.createBackplane();
if (this.backplane) {
@@ -301,12 +362,18 @@ export class IntentsGateway
}
}
- async handleConnection(client: WebSocket) {
- // ── Protocol version check (issue #456) ────────────────────────────────
- // `client.protocol` is the negotiated subprotocol string from the HTTP
- // upgrade handshake. Empty string means the client sent no
- // Sec-WebSocket-Protocol header — we default to vortex.v1.
- // Any other value that is not vortex.v1 is rejected with close code 1002.
+ handleConnection(client: WebSocket) {
+ // Graceful shutdown: refuse new clients once draining has begun (issue #511).
+ if (this.draining) {
+ client.close(1001, "Server draining");
+ this.metricsService?.wsConnectionsRejected.inc({ reason: "draining" });
+ return;
+ }
+
+ // Subprotocol negotiation (issue #456): resolveProtocol treats "" as the
+ // backward-compatible default vortex.v1; any other non-empty string that
+ // is not vortex.v1 was echoed back by negotiateProtocol and is rejected
+ // here with a descriptive close reason.
const protocolResult = resolveProtocol(
(client as unknown as { protocol?: string }).protocol ?? "",
);
@@ -328,9 +395,13 @@ export class IntentsGateway
this.alive.set(client, true);
this.metricsService?.incWsConnection();
- client.on("message", (raw) => {
- void this.handleMessage(client, raw);
- });
+ client.on("message", (raw) =>
+ this.handleMessage(client, raw).catch((err: unknown) =>
+ logger.warn(
+ `ws message handler failed: ${err instanceof Error ? err.message : String(err)}`,
+ ),
+ ),
+ );
client.on("pong", () => {
this.alive.set(client, true);
@@ -343,7 +414,7 @@ export class IntentsGateway
);
});
- const currentSeq = await this.replayStore.latestSeq();
+ const currentSeq = this.nextSeq - 1;
client.send(
JSON.stringify({
@@ -351,6 +422,9 @@ export class IntentsGateway
message: "Vortex intent stream",
version: negotiatedVersion,
seq: currentSeq,
+ // Negotiated solver heartbeat cadence (issue #445); dropped from the
+ // payload when the liveness service is not wired.
+ heartbeatIntervalMs: this.liveness?.heartbeatIntervalMs,
}),
);
@@ -429,6 +503,9 @@ export class IntentsGateway
case "auth":
await this.handleAuth(client, msg);
break;
+ case "heartbeat":
+ this.handleHeartbeat(client);
+ break;
default:
break;
}
@@ -542,6 +619,13 @@ export class IntentsGateway
/**
* Process a `{ type: "replay", fromSeq: number }` message.
+ *
+ * Events are read from the injected replay store when one is wired
+ * (issue #457 — memory or Redis, survives restarts and spans replicas),
+ * otherwise from the in-process ring buffer. Before the events are sent,
+ * the requesting connection's subscription filter (chains or solver
+ * capability predicate) is applied so replay honours the same
+ * server-side scoping as live delivery.
*/
private async handleReplay(client: WebSocket, msg: Record): Promise {
const fromSeq = typeof msg.fromSeq === "number" ? msg.fromSeq : null;
@@ -552,23 +636,47 @@ export class IntentsGateway
if (client.readyState !== WebSocket.OPEN) return;
- const result = await this.replayStore.since(fromSeq);
+ let events: SequencedEvent[];
+ if (this.replayStore) {
+ const result = await this.replayStore.since(fromSeq);
+ if (result.tooOld) {
+ const oldest = await this.replayStore.oldestSeq();
+ client.send(JSON.stringify({ type: "replay_too_old", fromSeq, oldestAvailableSeq: oldest }));
+ logger.debug(`ws replay_too_old: fromSeq=${fromSeq} oldestAvailable=${oldest}`);
+ return;
+ }
+ events = result.events;
+ } else {
+ const oldest = this.ringBuffer.oldestSeq();
- if (result.tooOld) {
- const oldest = await this.replayStore.oldestSeq();
- client.send(JSON.stringify({ type: "replay_too_old", fromSeq, oldestAvailableSeq: oldest }));
- logger.debug(`ws replay_too_old: fromSeq=${fromSeq} oldestAvailable=${oldest}`);
- return;
- }
+ if (oldest !== -1 && fromSeq < oldest - 1) {
+ client.send(
+ JSON.stringify({
+ type: "replay_too_old",
+ fromSeq,
+ oldestAvailableSeq: oldest,
+ }),
+ );
+ logger.debug(`ws replay_too_old: fromSeq=${fromSeq} oldestAvailable=${oldest}`);
+ return;
+ }
- const events = result.events;
+ events = this.ringBuffer.since(fromSeq);
+ }
- client.send(JSON.stringify({ type: "replay_start", fromSeq, count: events.length }));
+ client.send(
+ JSON.stringify({
+ type: "replay_start",
+ fromSeq,
+ count: events.length,
+ }),
+ );
const filter = this.subscribers.get(client);
for (const event of events) {
if (client.readyState !== WebSocket.OPEN) break;
- // Apply server-side filter (same logic as deliverToMatchingSubscribers but for one client)
+ // Apply server-side filter (same logic as deliverToMatchingSubscribers
+ // but for this one client).
if (filter) {
if (!filter.wantAll) {
if (filter.solver !== null) {
@@ -589,11 +697,69 @@ export class IntentsGateway
}
if (client.readyState === WebSocket.OPEN) {
- client.send(JSON.stringify({ type: "replay_end", count: events.length }));
+ client.send(
+ JSON.stringify({
+ type: "replay_end",
+ count: events.length,
+ }),
+ );
}
+
logger.debug(`ws replay complete: fromSeq=${fromSeq} count=${events.length}`);
}
+ /**
+ * Handles `{ "type": "heartbeat" }` from an authenticated solver
+ * (issue #445): refreshes liveness and acknowledges with the negotiated
+ * cadence. Unauthenticated connections get an explicit rejection so a bot
+ * notices it never completed auth.
+ */
+ private handleHeartbeat(client: WebSocket): void {
+ const address = this.authenticatedSolver.get(client);
+ if (!address) {
+ this.send(client, JSON.stringify({ type: "heartbeat_ack", accepted: false, reason: "not authenticated" }));
+ return;
+ }
+ void this.liveness?.touch(address).catch((err: unknown) =>
+ logger.warn(`liveness touch failed for ${address}: ${err instanceof Error ? err.message : String(err)}`),
+ );
+ this.send(
+ client,
+ JSON.stringify({
+ type: "heartbeat_ack",
+ accepted: true,
+ heartbeatIntervalMs: this.liveness?.heartbeatIntervalMs,
+ }),
+ );
+ }
+
+ /**
+ * Resolves a solver record for authentication (issue #445).
+ *
+ * Authenticating always touches the solver's liveness window. When the
+ * record was taken offline by missed heartbeats (the auto-offline flag is
+ * set) the touch heals it back to online — a successful signature is
+ * proof the process is running again — while a deliberately deactivated
+ * or deregistered record (no flag) stays rejected.
+ *
+ * @returns the active record, or `null` when the solver is unknown or
+ * deliberately inactive.
+ */
+ private async solverForAuth(
+ solver: string,
+ ): Promise>> | null> {
+ let record = await this.solversService.get(solver);
+ if (!record) return null;
+ await this.liveness?.touch(solver).catch((err: unknown) =>
+ logger.warn(`liveness touch failed for ${solver}: ${err instanceof Error ? err.message : String(err)}`),
+ );
+ if (!record.isActive) {
+ // touch() re-activates auto-offlined records; re-read to observe it.
+ record = (await this.solversService.get(solver)) ?? record;
+ }
+ return record.isActive ? record : null;
+ }
+
/**
* Authenticate a solver connection and install a capability predicate.
*
@@ -625,8 +791,8 @@ export class IntentsGateway
return;
}
- const solverRecord = await this.solversService.get(solver);
- if (!solverRecord || !solverRecord.isActive) {
+ const solverRecord = await this.solverForAuth(solver);
+ if (!solverRecord) {
client.send(JSON.stringify({ type: "auth_error", reason: "solver not registered or inactive" }));
return;
}
@@ -638,8 +804,12 @@ export class IntentsGateway
return;
}
- // Build capability predicate and store it on the connection.
- const predicate = buildMatchPredicate(solverRecord);
+ // Build capability predicate and store it on the connection (#445: gated by liveness).
+ const liveness = this.liveness;
+ const predicate = buildMatchPredicate(
+ solverRecord,
+ liveness ? () => liveness.isLive(solver) : undefined,
+ );
this.authenticatedSolver.set(client, solver);
const authFilter = this.subscribers.get(client);
this.subscribers.set(client, {
@@ -649,7 +819,14 @@ export class IntentsGateway
subscriptionCount: authFilter?.subscriptionCount ?? 0,
});
- client.send(JSON.stringify({ type: "auth_ok" }));
+ client.send(
+ JSON.stringify({
+ type: "auth_ok",
+ // Negotiated heartbeat cadence (issue #445); undefined values are
+ // dropped by JSON.stringify when liveness is not wired.
+ heartbeatIntervalMs: this.liveness?.heartbeatIntervalMs,
+ }),
+ );
// Send scoped snapshot of currently-eligible intents (issue #436).
try {
@@ -679,7 +856,11 @@ export class IntentsGateway
const solverRecord = await this.solversService.get(solverAddress);
if (!solverRecord) return;
- const predicate = buildMatchPredicate(solverRecord);
+ const liveness = this.liveness;
+ const predicate = buildMatchPredicate(
+ solverRecord,
+ liveness ? () => liveness.isLive(solverAddress) : undefined,
+ );
for (const [client, filter] of this.subscribers) {
if (this.authenticatedSolver.get(client) === solverAddress && filter.solver !== null) {
this.subscribers.set(client, { ...filter, solver: predicate });
@@ -751,8 +932,17 @@ export class IntentsGateway
// eligible-intents call sees fresh state.
this.updateIndexForEvent(event);
- // Push into replay store before sending.
- await this.replayStore.append(sequencedEvent);
+ // Push into replay buffer before sending.
+ this.ringBuffer.push(sequencedEvent);
+ // Mirror into the injected replay store (issue #457) so reconnecting
+ // clients can replay across restarts/replicas.
+ if (this.replayStore) {
+ try {
+ await this.replayStore.append(sequencedEvent);
+ } catch (err) {
+ logger.warn(`replay store append failed: ${String(err)}`);
+ }
+ }
logger.debug(`ws broadcast type=${event.type} seq=${seq} subscribers=${this.subscribers.size}`);
@@ -807,11 +997,35 @@ export class IntentsGateway
return this.subscribers.size;
}
+ /**
+ * Backplane health for /health (issues #454, #492).
+ *
+ * Delegates to the intent feed when it is wired — the feed owns the real
+ * backplane link (and its Redis connection state). Without a feed, fall
+ * back to this gateway's own view: a memory backplane is a single replica
+ * and is healthy by construction; the gateway's redis mirror reports its
+ * sequenced high-water mark.
+ */
+ backplaneHealth(): BackplaneHealth {
+ if (this.feed) return this.feed.backplaneHealth();
+ return {
+ mode: this.backplane ? "redis" : "memory",
+ status: "ok",
+ lastSeq: this.nextSeq - 1,
+ pendingPublishes: 0,
+ };
+ }
+
/** Returns the current number of active WebSocket subscribers. */
get subscriberCount(): number {
return this.subscribers.size;
}
+ /** Send a JSON frame when the socket is open (heartbeat acks). */
+ private send(client: WebSocket, payload: string): void {
+ if (client.readyState === WebSocket.OPEN) client.send(payload);
+ }
+
private heartbeat() {
for (const [client] of this.subscribers) {
if (this.alive.get(client) === false) {
@@ -830,11 +1044,79 @@ export class IntentsGateway
}
}
- onModuleDestroy() {
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
+ /**
+ * Graceful connection draining (issue #511):
+ * 1. Set the draining flag so `handleConnection` refuses new clients
+ * 2. Send every connected client a `server_draining` event carrying the
+ * resume sequence so it can reconnect with replay
+ * 3. Close connections in batches with jittered delays to avoid a
+ * reconnect stampede against the replacement pod
+ *
+ * Kubernetes terminationGracePeriodSeconds should be at least
+ * WS_DRAIN_TIMEOUT_MS + 5s.
+ */
+ async startDraining(): Promise {
+ if (this.draining) return;
+
+ this.draining = true;
+ const drainTimeoutMs = parseInt(process.env.WS_DRAIN_TIMEOUT_MS ?? "25000", 10);
+ const currentSeq = this.nextSeq - 1;
+ const clientCount = this.subscribers.size;
+
+ logger.warn(`ws draining started: ${clientCount} clients, timeout=${drainTimeoutMs}ms`);
+
+ const drainMessage = JSON.stringify({
+ type: "server_draining",
+ resumeFrom: currentSeq,
+ reconnectAfterMs: Math.floor(1000 + Math.random() * 4000),
+ reason: "graceful_shutdown",
+ });
+
for (const [client] of this.subscribers) {
- client.close(1001, "Server shutting down");
- this.removeSubscriber(client);
+ if (client.readyState === WebSocket.OPEN) {
+ try {
+ client.send(drainMessage);
+ } catch {
+ /* best effort — the connection is going away anyway */
+ }
+ }
}
+
+ if (clientCount === 0) {
+ logger.warn("ws draining complete: no clients connected");
+ return;
+ }
+
+ const batchSize = Math.max(10, Math.ceil(clientCount / 10));
+ const clients = Array.from(this.subscribers.keys());
+ const batchDelayMs = Math.floor(drainTimeoutMs / Math.ceil(clientCount / batchSize));
+
+ for (let i = 0; i < clients.length; i += batchSize) {
+ const batch = clients.slice(i, i + batchSize);
+ if (i > 0) await new Promise((r) => setTimeout(r, batchDelayMs));
+
+ for (const client of batch) {
+ if (client.readyState === WebSocket.OPEN) {
+ client.close(1001, "Server draining");
+ }
+ this.removeSubscriber(client);
+ }
+
+ logger.info(
+ `ws drain progress: ${Math.min(i + batchSize, clientCount)}/${clientCount} closed`,
+ );
+ }
+
+ logger.warn("ws draining complete: all clients closed");
+ }
+
+ /** True while the gateway refuses new connections during shutdown (issue #511). */
+ isDraining(): boolean {
+ return this.draining;
+ }
+
+ async onModuleDestroy() {
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
+ await this.startDraining();
}
}
diff --git a/src/intents/intents.module.ts b/src/intents/intents.module.ts
index 4ea94aa0..9bbc95d9 100644
--- a/src/intents/intents.module.ts
+++ b/src/intents/intents.module.ts
@@ -1,38 +1,3 @@
-import { Module } from "@nestjs/common";
-import { IntentsService } from "./intents.service";
-import { IntentsController } from "./intents.controller";
-import { IntentsGateway } from "./intents.gateway";
-import { INTENTS_REPOSITORY, InMemoryIntentsRepository } from "./intents.repository";
-import { PrismaIntentsRepository } from "./prisma-intents.repository";
-import { DualWriteIntentsRepository } from "./dual-write-intents.repository";
-import { PrismaService } from "../prisma/prisma.service";
-
-/**
- * IntentsModule wires the intents feature slice.
- *
- * The active repository adapter is selected at startup via INTENTS_STORE:
- * memory — InMemoryIntentsRepository (default, dev/test)
- * dual — DualWriteIntentsRepository (migration phase)
- * postgres — PrismaIntentsRepository (production)
- *
- * IntentsGateway is exported so StatsModule can inject it for subscriber counts.
- */
-@Module({
- controllers: [IntentsController],
- providers: [
- {
- provide: INTENTS_REPOSITORY,
- inject: [PrismaService],
- useFactory: (prisma: PrismaService) => {
- const store = process.env.INTENTS_STORE ?? process.env.INTENTS_PERSISTENCE ?? "memory";
- if (store === "postgres") {
- return new PrismaIntentsRepository(prisma);
- }
- if (store === "dual") {
- const primary = new InMemoryIntentsRepository({ seed: false });
- const secondary = new PrismaIntentsRepository(prisma);
- return new DualWriteIntentsRepository(primary, secondary);
- }
import { Module, forwardRef } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { IntentsService } from "./intents.service";
@@ -85,28 +50,20 @@ import { RedisReplayStore } from "./backplane/redis-replay.store";
return new InMemoryIntentsRepository();
},
},
- IntentsService,
- IntentsGateway,
- ],
- exports: [IntentsService, IntentsGateway, INTENTS_REPOSITORY],
- IntentCapabilityIndex,
- IntentsGateway,
- IntentsSweeperService,
- IntentsMaintenanceJobs,
- // Note: EventIngestionService is provided by SorobanModule (imported above)
- // and exported from there — no re-declaration needed here.
+ // Sequenced WS replay store (issue #457) — memory by default, Redis when
+ // WS_REPLAY_STORE=redis. Injected into IntentsGateway via REPLAY_STORE.
{
provide: REPLAY_STORE,
useFactory: () => {
- const store = (process.env.WS_REPLAY_STORE ?? 'memory').toLowerCase();
- const maxCount = parseInt(process.env.WS_REPLAY_MAX_COUNT ?? '500', 10);
+ const store = (process.env.WS_REPLAY_STORE ?? "memory").toLowerCase();
+ const maxCount = parseInt(process.env.WS_REPLAY_MAX_COUNT ?? "500", 10);
const maxAgeMs = process.env.WS_REPLAY_MAX_AGE_MS
? parseInt(process.env.WS_REPLAY_MAX_AGE_MS, 10)
: undefined;
- if (store === 'redis') {
+ if (store === "redis") {
return new RedisReplayStore({
- redisUrl: process.env.REDIS_URL ?? 'redis://localhost:6379',
- streamKey: 'vortex:intents:replay',
+ redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379",
+ streamKey: "vortex:intents:replay",
maxCount,
maxAgeMs,
});
@@ -114,6 +71,13 @@ import { RedisReplayStore } from "./backplane/redis-replay.store";
return new MemoryReplayStore({ maxCount, maxAgeMs });
},
},
+ IntentsService,
+ IntentCapabilityIndex,
+ IntentsGateway,
+ IntentsSweeperService,
+ IntentsMaintenanceJobs,
+ // Note: EventIngestionService is provided by SorobanModule (imported above)
+ // and exported from there — no re-declaration needed here.
],
exports: [IntentsService, IntentsGateway, IntentCapabilityIndex, REPLAY_STORE],
})
diff --git a/src/intents/intents.repository.ts b/src/intents/intents.repository.ts
index 1e31f7a5..b91b0803 100644
--- a/src/intents/intents.repository.ts
+++ b/src/intents/intents.repository.ts
@@ -1,24 +1,30 @@
import { Injectable } from "@nestjs/common";
import { v4 as uuidv4 } from "uuid";
-import { Intent, IntentState } from "./intents.types";
+import { Intent, IntentState, SupportedChain } from "./intents.types";
import { buildSeedIntents } from "./intents.seed";
/**
* NestJS injection token for the intents repository.
*
+ * Use this token instead of a concrete class so any module can swap
+ * InMemoryIntentsRepository for a Prisma-backed adapter without touching
+ * IntentsService.
+ *
* @example
* \@Inject(INTENTS_REPOSITORY) private readonly repo: IIntentsRepository
*/
export const INTENTS_REPOSITORY = Symbol("INTENTS_REPOSITORY");
-// ─── Optimistic-concurrency types (#404) ─────────────────────────────────────
+type MaybePromise = T | Promise;
/**
- * Returned by mutations when the caller's `expectedVersion` does not match
- * the row's actual version. The caller should re-read the row, reconcile,
- * and retry.
+ * Returned by a mutation whose `expectedVersion` no longer matches the stored
+ * record (issue #405). Carries the version actually found so callers can
+ * decide whether to re-read and retry or surface a conflict to the client.
*/
export class VersionConflict {
+ readonly kind = "version_conflict" as const;
+
constructor(
readonly intentId: string,
readonly expectedVersion: number,
@@ -26,110 +32,190 @@ export class VersionConflict {
) {}
}
-export function isVersionConflict(result: unknown): result is VersionConflict {
- return result instanceof VersionConflict;
+/** Type guard for {@link VersionConflict}. */
+export function isVersionConflict(value: unknown): value is VersionConflict {
+ return value instanceof VersionConflict;
}
/**
- * Union of successful intent result and version conflict.
- * All guarded mutation methods return this type.
+ * Result of a mutation: the updated intent, `null` when the intent does not
+ * exist or its state guard failed, or a {@link VersionConflict} when an
+ * `expectedVersion` was supplied and did not match.
*/
-export type MutationResult = Intent | VersionConflict | null;
+export type MutationResult = Intent | null | VersionConflict;
-// ─── Idempotency (#404) ───────────────────────────────────────────────────────
+/** Fields a generic `update()` may change — identity and version are managed by the repository. */
+export type IntentPatch = Omit, "intentId" | "version" | "createdAt">;
+/** Result of {@link IIntentsRepository.createIdempotent}. */
export interface IdempotentCreateResult {
intent: Intent;
- /** true when a new row was created; false when the key already existed (replay). */
+ /** false when an unexpired intent already held the idempotency key. */
created: boolean;
}
-// ─── Patch type ───────────────────────────────────────────────────────────────
-
-export type IntentPatch = Partial>;
+/**
+ * Filter / sort / pagination parameters for advanced intent search (issue #440).
+ *
+ * All fields are optional; when none are present the search returns every
+ * intent sorted by `createdAt` descending — identical to the pre-existing
+ * default behaviour.
+ */
+export interface IntentSearchQuery {
+ state?: IntentState;
+ user?: string;
+ chain?: SupportedChain;
+ /** Minimum USD value at creation (inclusive). */
+ minAmountUsd?: number;
+ /** Maximum USD value at creation (inclusive). */
+ maxAmountUsd?: number;
+ /** Minimum creation time, unix epoch seconds (inclusive). */
+ createdFrom?: number;
+ /** Maximum creation time, unix epoch seconds (inclusive). */
+ createdTo?: number;
+ /** Source token symbol (case-insensitive). */
+ srcToken?: string;
+ /** Destination token symbol (case-insensitive). */
+ dstToken?: string;
+ /** Solver address that accepted/filled the intent. */
+ solver?: string;
+ /**
+ * Sort dimension and direction: `created` | `deadline` | `usd`, optionally
+ * with an `:asc` / `:desc` suffix. Defaults to `created:desc`.
+ */
+ sort?: string;
+ limit?: number;
+ offset?: number;
+}
-// ─── Repository interface ─────────────────────────────────────────────────────
+/** A page of search results plus the total number of matching intents. */
+export interface IntentSearchResult {
+ intents: Intent[];
+ total: number;
+}
+/**
+ * Storage contract for intent records.
+ *
+ * Every mutation increments `version` by exactly one, and every mutation
+ * accepts an expected version so concurrent writers (HTTP handlers, the
+ * sweeper, event ingestion, the deposit verifier) can never silently
+ * overwrite one another (issue #405). Implementations must apply the state
+ * guard, the version check, and the write as a single atomic step — for SQL
+ * that means one `UPDATE … WHERE … RETURNING *` statement.
+ *
+ * All methods are synchronous for the in-memory adapter and return Promises
+ * for the Prisma adapter — callers always `await` so both shapes work. The
+ * shared contract suite in `intents-repository.contract.ts` runs against
+ * every implementation.
+ */
export interface IIntentsRepository {
- save(intent: Intent): Intent | Promise;
- findById(id: string): Intent | undefined | Promise;
- findAll(): Intent[] | Promise;
- findByState(state: IntentState): Intent[] | Promise;
- findByUser(user: string): Intent[] | Promise;
- findManyByIds(ids: string[]): Intent[] | Promise;
- countAcceptedBySolver(solver: string): number | Promise;
- countActiveByUser(user: string): number | Promise;
+ /**
+ * Persist a fully-formed intent record exactly as given (including its
+ * `version`) and return it. Used for creation and for mirroring rows between
+ * stores; it is not a mutation path — use {@link update} to change a record.
+ */
+ save(intent: Intent): MaybePromise;
/**
- * Idempotent create: inserts only when no row exists for `idempotencyKey`
- * with `createdAt >= minCreatedAt`. Returns the existing row on replay.
+ * Insert `intent` unless another intent created at or after
+ * `minCreatedAt` already holds `idempotencyKey`, in which case that intent
+ * is returned instead. Must be atomic across replicas (a unique index on the
+ * key in SQL). A key held by an older intent is released and reused.
*/
createIdempotent(
intent: Intent,
idempotencyKey: string,
minCreatedAt: number,
- ): IdempotentCreateResult | Promise;
+ ): MaybePromise;
- findByIdempotencyKey(
- key: string,
- minCreatedAt: number,
- ): Intent | undefined | Promise;
+ /** Find the unexpired intent created with `idempotencyKey`, if any. */
+ findByIdempotencyKey(idempotencyKey: string, minCreatedAt: number): MaybePromise;
+
+ /**
+ * Find an intent by its unique intentId.
+ * Returns `undefined` when no matching record exists.
+ */
+ findById(id: string): MaybePromise;
+
+ /** Fetch several intents in one call; unknown IDs are omitted. */
+ findManyByIds(ids: string[]): MaybePromise;
/**
- * Apply a partial patch. Returns VersionConflict on stale write, null when
- * the intent is not found.
- * Atomically replace an open intent's minimum output and deadline while its
- * current deadline is still in the future. Returns null when the intent is
- * missing, no longer open, or already expired.
+ * Return all intent records sorted by createdAt descending.
+ */
+ findAll(): MaybePromise;
+
+ /**
+ * Return all intents matching the given state, sorted by createdAt descending.
+ */
+ findByState(state: IntentState): MaybePromise;
+
+ /**
+ * Return all intents belonging to the given user (case-insensitive address match).
+ */
+ findByUser(user: string): MaybePromise;
+
+ /** Number of intents currently `accepted` by `solver`. */
+ countAcceptedBySolver(solver: string): MaybePromise;
+
+ /** Number of `open` or `accepted` intents owned by `user` (case-insensitive). */
+ countActiveByUser(user: string): MaybePromise;
+
+ /**
+ * Apply `patch` only if the stored version equals `expectedVersion`:
+ * UPDATE intents SET …patch, version = version + 1
+ * WHERE intent_id = $1 AND version = $2 RETURNING *
+ * Returns `null` when the intent does not exist.
+ */
+ update(id: string, patch: IntentPatch, expectedVersion?: number): MaybePromise;
+
+ /**
+ * Amend the user-adjustable fields of an `open` intent while its deadline is
+ * still in the future (issue #569): only `minDstAmount` and `deadline` may
+ * change, and a new deadline must also lie in the future. Returns `null`
+ * when the intent does not exist, is not open, or either deadline check
+ * fails; the repository does not bump `version` (the amend is a widening of
+ * terms, not a competing solver write).
*/
amendIfOpen(
id: string,
patch: Pick,
now?: number,
- ): Intent | null | Promise;
+ ): MaybePromise;
/**
* Remove a stored intent. Used only for in-memory retention sweeps for stale
- * terminal-state records; Prisma-backed stores ignore this call by design.
+ * terminal-state records.
*/
- update(
- id: string,
- patch: IntentPatch,
- expectedVersion: number,
- ): MutationResult | Promise;
-
- delete(id: string): boolean | Promise;
-
- // ── Atomic state transitions ──────────────────────────────────────────────
+ delete(id: string): MaybePromise;
+ /**
+ * Atomically transition an intent from `open` → `accepted` while its
+ * deadline is still in the future (issue #473):
+ * UPDATE intents SET state='accepted', solver=$2, deadline=$3, version=version+1
+ * WHERE intent_id=$1 AND state='open' AND deadline > $now [AND version=$v] RETURNING *
+ * Returns `null` when the intent is not found, already taken, or past its
+ * deadline (the sweeper wins that race). `now` defaults to the current time.
+ *
+ * Lock ordering (issue #473): callers holding a per-solver advisory lock
+ * must acquire it BEFORE invoking this method; this method itself only
+ * touches the single intent row so no lock inversion is possible.
+ */
acceptIfOpen(
id: string,
solver: string,
newDeadline: number,
now?: number,
expectedVersion?: number,
- ): MutationResult | Promise;
-
- acceptIfOpenWithinExposure(
- id: string,
- solver: string,
- newDeadline: number,
- now: number,
- candidateExposureUsdMicros: bigint,
- maxExposureUsdMicros: bigint,
- ): Promise<{ intent: Intent | null; exposureExceeded: boolean }> | { intent: Intent | null; exposureExceeded: boolean };
+ ): MaybePromise;
/**
* Atomically transition an intent from `accepted` → `filled` only if it is
* currently accepted by the specified solver AND the fill window has not
- * elapsed. Mirrors the DB pattern:
- * UPDATE intents SET state='filled', ...patch
- * WHERE intent_id=$1 AND state='accepted' AND solver=$2 AND deadline > $3
- * RETURNING *
- * Returns the updated intent on success, `null` on any guard failure.
- * The `minDstAmount` invariant (fill >= minDst) is enforced by the
- * controller/service layer with bigint comparison before this write; the
- * state+deadline predicates here make the write itself race-free.
+ * elapsed (issue #473). The `minDstAmount` invariant is enforced by the
+ * controller before this write; the state + deadline predicates make the
+ * write itself race-free.
*/
fillIfAccepted(
id: string,
@@ -137,42 +223,63 @@ export interface IIntentsRepository {
patch: Pick, "filledAt" | "fillAmount" | "feeAmount" | "txHash">,
now?: number,
expectedVersion?: number,
- ): MutationResult | Promise;
-
- cancelIfOpen(id: string, expectedVersion?: number): MutationResult | Promise;
-
- expireIfOpen(id: string, expectedVersion?: number): MutationResult | Promise;
+ ): MaybePromise;
+ /**
+ * Atomically push an accepted intent's deadline out to `newDeadline`, only
+ * while it is still `accepted` and its deadline is earlier (issue #477):
+ * UPDATE intents SET deadline=$2, version=version+1
+ * WHERE intent_id=$1 AND state='accepted' AND deadline < $2 [AND version=$v] RETURNING *
+ * While an emergency pause covers `fill`, the sweeper extends windows instead
+ * of slashing. The `deadline < $2` guard makes repeat calls no-ops and never
+ * shortens a window; a concurrent fill or slash wins via the state predicate.
+ */
extendDeadlineIfAccepted(
id: string,
newDeadline: number,
expectedVersion?: number,
- ): MutationResult | Promise;
+ ): MaybePromise;
+
+ /** Atomically transition an intent from `open` → `cancelled`. */
+ cancelIfOpen(id: string, expectedVersion?: number): MaybePromise;
+
+ /**
+ * Atomically transition an intent from `open` → `expired`. Guards the
+ * sweeper's expiry pass against a concurrent user cancel() or solver accept().
+ */
+ expireIfOpen(id: string, expectedVersion?: number): MaybePromise;
+ /**
+ * Atomically transition an intent from `accepted` → `slashed`. Guards the
+ * sweeper's slashing pass against a concurrent solver fill().
+ */
slashIfAccepted(
id: string,
patch: { slashedAt: number; slashReason: string },
expectedVersion?: number,
- ): MutationResult | Promise;
-
- /**
- * Version-guarded upsert used by the dual-write mirror.
- * Only saves when the incoming version is >= the stored version.
- */
- saveIfNewer?(intent: Intent): Promise;
+ ): MaybePromise;
}
-// ─── In-memory implementation ─────────────────────────────────────────────────
+/** Options for {@link InMemoryIntentsRepository}. */
+export interface InMemoryIntentsRepositoryOptions {
+ /** Seed demo intents on construction (default true). Disabled in `dual` mode. */
+ seed?: boolean;
+}
+/**
+ * In-memory implementation of IIntentsRepository.
+ *
+ * Stores intents in a plain `Map`. Every method runs synchronously, so each
+ * check-and-write is atomic within the Node.js event loop — this is the
+ * reference behaviour the Prisma adapter reproduces with single SQL statements.
+ */
@Injectable()
export class InMemoryIntentsRepository implements IIntentsRepository {
private readonly store = new Map();
- /** key → intentId (idempotency replay cache) */
private readonly idempotencyKeys = new Map();
- constructor(options?: { seed?: boolean }) {
- const shouldSeed = options?.seed !== false;
- if (shouldSeed) this.seed();
+ constructor(options: InMemoryIntentsRepositoryOptions = {}) {
+ if (options.seed ?? true) this.seed();
}
save(intent: Intent): Intent {
@@ -180,10 +287,35 @@ export class InMemoryIntentsRepository implements IIntentsRepository {
return intent;
}
+ createIdempotent(intent: Intent, idempotencyKey: string, minCreatedAt: number): IdempotentCreateResult {
+ const existing = this.findByIdempotencyKey(idempotencyKey, minCreatedAt);
+ if (existing) return { intent: existing, created: false };
+ this.store.set(intent.intentId, intent);
+ this.idempotencyKeys.set(idempotencyKey, intent.intentId);
+ return { intent, created: true };
+ }
+
+ findByIdempotencyKey(idempotencyKey: string, minCreatedAt: number): Intent | undefined {
+ const intentId = this.idempotencyKeys.get(idempotencyKey);
+ if (!intentId) return undefined;
+ const intent = this.store.get(intentId);
+ if (!intent || intent.createdAt < minCreatedAt) {
+ this.idempotencyKeys.delete(idempotencyKey);
+ return undefined;
+ }
+ return intent;
+ }
+
findById(id: string): Intent | undefined {
return this.store.get(id);
}
+ findManyByIds(ids: string[]): Intent[] {
+ return [...new Set(ids)]
+ .map((id) => this.store.get(id))
+ .filter((intent): intent is Intent => intent !== undefined);
+ }
+
findAll(): Intent[] {
return [...this.store.values()].sort((a, b) => b.createdAt - a.createdAt);
}
@@ -193,61 +325,94 @@ export class InMemoryIntentsRepository implements IIntentsRepository {
}
findByUser(user: string): Intent[] {
- const lower = user.toLowerCase();
- return this.findAll().filter((i) => i.user.toLowerCase() === lower);
+ return this.findAll().filter((i) => i.user.toLowerCase() === user.toLowerCase());
}
- findManyByIds(ids: string[]): Intent[] {
- const unique = [...new Set(ids)];
- return unique.flatMap((id) => {
- const i = this.store.get(id);
- return i ? [i] : [];
+ /**
+ * Advanced search with filtering, sorting and pagination (issue #440).
+ * Mirrors the SQL adapter's semantics in memory so both stores satisfy
+ * `intents-search.spec.ts`.
+ */
+ search(query: IntentSearchQuery): IntentSearchResult {
+ let results = this.findAll();
+
+ if (query.state !== undefined) results = results.filter((i) => i.state === query.state);
+ if (query.user !== undefined) {
+ const needle = query.user.toLowerCase();
+ results = results.filter((i) => i.user.toLowerCase() === needle);
+ }
+ if (query.chain !== undefined) results = results.filter((i) => i.srcChain === query.chain);
+ if (query.solver !== undefined) {
+ const needle = query.solver.toLowerCase();
+ results = results.filter((i) => i.solver !== undefined && i.solver.toLowerCase() === needle);
+ }
+ if (query.minAmountUsd !== undefined) {
+ results = results.filter(
+ (i) => i.usdValueAtCreate !== undefined && i.usdValueAtCreate >= query.minAmountUsd!,
+ );
+ }
+ if (query.maxAmountUsd !== undefined) {
+ results = results.filter(
+ (i) => i.usdValueAtCreate !== undefined && i.usdValueAtCreate <= query.maxAmountUsd!,
+ );
+ }
+ if (query.createdFrom !== undefined) results = results.filter((i) => i.createdAt >= query.createdFrom!);
+ if (query.createdTo !== undefined) results = results.filter((i) => i.createdAt <= query.createdTo!);
+ if (query.srcToken !== undefined) {
+ const needle = query.srcToken.toLowerCase();
+ results = results.filter((i) => i.srcToken.symbol.toLowerCase() === needle);
+ }
+ if (query.dstToken !== undefined) {
+ const needle = query.dstToken.toLowerCase();
+ results = results.filter((i) => i.dstToken.symbol.toLowerCase() === needle);
+ }
+
+ // Sorting — default createdAt desc (preserves pre-existing behaviour).
+ const [dimension, direction] = (query.sort ?? "created:desc").split(":");
+ const dir = direction === "asc" ? 1 : -1;
+ results.sort((a, b) => {
+ switch (dimension) {
+ case "deadline":
+ return (a.deadline - b.deadline) * dir;
+ case "usd": {
+ const av = a.usdValueAtCreate ?? 0;
+ const bv = b.usdValueAtCreate ?? 0;
+ return (av - bv) * dir;
+ }
+ case "created":
+ default:
+ return (a.createdAt - b.createdAt) * dir;
+ }
});
+
+ const total = results.length;
+ const offset = query.offset ?? 0;
+ const limit = query.limit ?? 20;
+ const page = results.slice(offset, offset + limit);
+ return { intents: page, total };
}
countAcceptedBySolver(solver: string): number {
- const lower = solver.toLowerCase();
- return [...this.store.values()].filter(
- (i) => i.state === "accepted" && i.solver?.toLowerCase() === lower,
- ).length;
+ let count = 0;
+ for (const intent of this.store.values()) {
+ if (intent.state === "accepted" && intent.solver === solver) count++;
+ }
+ return count;
}
countActiveByUser(user: string): number {
- const lower = user.toLowerCase();
- return [...this.store.values()].filter(
- (i) => (i.state === "open" || i.state === "accepted") && i.user.toLowerCase() === lower,
- ).length;
- }
-
- createIdempotent(
- intent: Intent,
- idempotencyKey: string,
- minCreatedAt: number,
- ): IdempotentCreateResult {
- const existing = this.findByIdempotencyKey(idempotencyKey, minCreatedAt);
- if (existing) return { intent: existing, created: false };
- this.save(intent);
- this.idempotencyKeys.set(idempotencyKey, intent.intentId);
- return { intent, created: true };
- }
-
- findByIdempotencyKey(key: string, minCreatedAt: number): Intent | undefined {
- const id = this.idempotencyKeys.get(key);
- if (!id) return undefined;
- const intent = this.store.get(id);
- if (!intent || intent.createdAt < minCreatedAt) return undefined;
- return intent;
+ const needle = user.toLowerCase();
+ let count = 0;
+ for (const intent of this.store.values()) {
+ if ((intent.state === "open" || intent.state === "accepted") && intent.user.toLowerCase() === needle) {
+ count++;
+ }
+ }
+ return count;
}
- update(id: string, patch: IntentPatch, expectedVersion: number): MutationResult {
- const existing = this.store.get(id);
- if (!existing) return null;
- if (existing.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, existing.version);
- }
- const updated: Intent = { ...existing, ...patch, version: existing.version + 1 };
- this.store.set(id, updated);
- return updated;
+ update(id: string, patch: IntentPatch, expectedVersion?: number): MutationResult {
+ return this.mutate(id, expectedVersion, () => true, (existing) => ({ ...existing, ...patch }));
}
amendIfOpen(
@@ -268,16 +433,6 @@ export class InMemoryIntentsRepository implements IIntentsRepository {
return this.store.delete(id);
}
- saveIfNewer(intent: Intent): Promise {
- const existing = this.store.get(intent.intentId);
- if (!existing || intent.version >= existing.version) {
- this.store.set(intent.intentId, intent);
- }
- return Promise.resolve(this.store.get(intent.intentId)!);
- }
-
- // ── Atomic transitions ────────────────────────────────────────────────────
-
acceptIfOpen(
id: string,
solver: string,
@@ -285,55 +440,13 @@ export class InMemoryIntentsRepository implements IIntentsRepository {
now?: number,
expectedVersion?: number,
): MutationResult {
- const existing = this.store.get(id);
- if (!existing || existing.state !== "open") return null;
const nowSec = now ?? Math.floor(Date.now() / 1000);
- if (existing.deadline <= nowSec) return null;
- if (expectedVersion !== undefined && existing.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, existing.version);
- }
- const updated: Intent = {
- ...existing,
- state: "accepted",
- solver,
- deadline: newDeadline,
- version: existing.version + 1,
- };
- this.store.set(id, updated);
- return updated;
- }
-
- acceptIfOpenWithinExposure(
- id: string,
- solver: string,
- newDeadline: number,
- now: number,
- candidateExposureUsdMicros: bigint,
- maxExposureUsdMicros: bigint,
- ): { intent: Intent | null; exposureExceeded: boolean } {
- const existing = this.store.get(id);
- if (!existing || existing.state !== "open" || existing.deadline <= now) {
- return { intent: null, exposureExceeded: false };
- }
- const lower = solver.toLowerCase();
- let acceptedExposure = 0n;
- for (const intent of this.store.values()) {
- if (intent.state === "accepted" && intent.solver?.toLowerCase() === lower) {
- acceptedExposure += intentExposureUsdMicros(intent, now);
- }
- }
- if (acceptedExposure + candidateExposureUsdMicros > maxExposureUsdMicros) {
- return { intent: null, exposureExceeded: true };
- }
- const updated: Intent = {
- ...existing,
- state: "accepted",
- solver,
- deadline: newDeadline,
- version: existing.version + 1,
- };
- this.store.set(id, updated);
- return { intent: updated, exposureExceeded: false };
+ return this.mutate(
+ id,
+ expectedVersion,
+ (i) => i.state === "open" && i.deadline > nowSec,
+ (i) => ({ ...i, state: "accepted", solver, deadline: newDeadline }),
+ );
}
fillIfAccepted(
@@ -343,68 +456,70 @@ export class InMemoryIntentsRepository implements IIntentsRepository {
now?: number,
expectedVersion?: number,
): MutationResult {
- const existing = this.store.get(id);
- if (!existing || existing.state !== "accepted" || existing.solver !== solver) return null;
const nowSec = now ?? Math.floor(Date.now() / 1000);
- if (existing.deadline <= nowSec) return null;
- if (expectedVersion !== undefined && existing.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, existing.version);
- }
- const updated: Intent = { ...existing, ...patch, state: "filled", version: existing.version + 1 };
- this.store.set(id, updated);
- return updated;
+ return this.mutate(
+ id,
+ expectedVersion,
+ (i) => i.state === "accepted" && i.solver === solver && i.deadline > nowSec,
+ (i) => ({ ...i, ...patch, state: "filled" }),
+ );
}
- cancelIfOpen(id: string, expectedVersion?: number): MutationResult {
- const existing = this.store.get(id);
- if (!existing || existing.state !== "open") return null;
- if (expectedVersion !== undefined && existing.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, existing.version);
- }
- const updated: Intent = { ...existing, state: "cancelled", version: existing.version + 1 };
- this.store.set(id, updated);
- return updated;
+ extendDeadlineIfAccepted(id: string, newDeadline: number, expectedVersion?: number): MutationResult {
+ return this.mutate(
+ id,
+ expectedVersion,
+ (i) => i.state === "accepted" && i.deadline < newDeadline,
+ (i) => ({ ...i, deadline: newDeadline }),
+ );
}
- expireIfOpen(id: string, expectedVersion?: number): MutationResult {
- const existing = this.store.get(id);
- if (!existing || existing.state !== "open") return null;
- if (expectedVersion !== undefined && existing.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, existing.version);
- }
- const updated: Intent = { ...existing, state: "expired", version: existing.version + 1 };
- this.store.set(id, updated);
- return updated;
+ cancelIfOpen(id: string, expectedVersion?: number): MutationResult {
+ return this.mutate(id, expectedVersion, (i) => i.state === "open", (i) => ({ ...i, state: "cancelled" }));
}
- extendDeadlineIfAccepted(id: string, newDeadline: number, expectedVersion?: number): MutationResult {
- const existing = this.store.get(id);
- if (!existing || existing.state !== "accepted") return null;
- if (existing.deadline >= newDeadline) return null;
- if (expectedVersion !== undefined && existing.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, existing.version);
- }
- const updated: Intent = { ...existing, deadline: newDeadline, version: existing.version + 1 };
- this.store.set(id, updated);
- return updated;
+ expireIfOpen(id: string, expectedVersion?: number): MutationResult {
+ return this.mutate(id, expectedVersion, (i) => i.state === "open", (i) => ({ ...i, state: "expired" }));
}
slashIfAccepted(
id: string,
patch: { slashedAt: number; slashReason: string },
expectedVersion?: number,
+ ): MutationResult {
+ return this.mutate(
+ id,
+ expectedVersion,
+ (i) => i.state === "accepted",
+ (i) => ({ ...i, ...patch, state: "slashed" }),
+ );
+ }
+
+ /**
+ * Shared check-and-write. Mirrors the SQL adapter's classification: a
+ * version mismatch is reported as a conflict before the state guard is
+ * consulted, exactly as `WHERE version = $v AND state = …` followed by a
+ * classifying re-read behaves.
+ */
+ private mutate(
+ id: string,
+ expectedVersion: number | undefined,
+ guard: (intent: Intent) => boolean,
+ apply: (intent: Intent) => Intent,
): MutationResult {
const existing = this.store.get(id);
- if (!existing || existing.state !== "accepted") return null;
- if (expectedVersion !== undefined && existing.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, existing.version);
+ if (!existing) return null;
+ const existingVersion = existing.version ?? 0;
+ if (expectedVersion !== undefined && existingVersion !== expectedVersion) {
+ return new VersionConflict(id, expectedVersion, existingVersion);
}
- const updated: Intent = { ...existing, ...patch, state: "slashed", version: existing.version + 1 };
+ if (!guard(existing)) return null;
+ const updated: Intent = { ...apply(existing), intentId: id, version: existingVersion + 1 };
this.store.set(id, updated);
return updated;
}
- // ── Seed ────────────────────────────────────────────────────────────────────
+ // ── seed ────────────────────────────────────────────────────────────────────
seed(): void {
const now = Math.floor(Date.now() / 1000);
@@ -415,6 +530,7 @@ export class InMemoryIntentsRepository implements IIntentsRepository {
createdAt: now - Math.floor(Math.random() * 600),
version: 0,
srcVerified: true,
+ srcVerification: { status: "skipped", checkedAt: now, detail: "demo seed data" },
};
this.store.set(intent.intentId, intent);
}
diff --git a/src/intents/intents.service.spec.ts b/src/intents/intents.service.spec.ts
index e69de29b..36588b18 100644
--- a/src/intents/intents.service.spec.ts
+++ b/src/intents/intents.service.spec.ts
@@ -0,0 +1,555 @@
+import { Test, TestingModule } from "@nestjs/testing";
+import { ConfigService } from "@nestjs/config";
+import { Keypair } from "@stellar/stellar-sdk";
+import { AppConfig, CHAIN_FILL_WINDOW_DEFAULTS, DEFAULT_FILL_WINDOW_SECONDS } from "../config/configuration";
+import { StellarTxService } from "../soroban/stellar-tx.service";
+import { IntentsService } from "./intents.service";
+import { INTENTS_REPOSITORY, InMemoryIntentsRepository } from "./intents.repository";
+import { PrismaService } from "../prisma/prisma.service";
+import { ProtocolParamsService } from "../governance/params.service";
+
+const VALID_CONTRACT_ID = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
+
+function fakeConfig(overrides: { onchainIntentsEnabled?: boolean; settlementContractId?: string } = {}) {
+ const values: Record = {
+ onchainIntentsEnabled: overrides.onchainIntentsEnabled ?? false,
+ "stellar.settlementContractId": overrides.settlementContractId ?? "",
+ };
+ return { get: (path: string) => values[path] } as ConfigService;
+}
+
+function fakeStellarTxService() {
+ return { invokeContract: jest.fn() } as unknown as jest.Mocked;
+}
+
+function fakePrismaService(): PrismaService {
+ return {
+ intentAuditLog: {
+ create: jest.fn().mockResolvedValue({}),
+ findMany: jest.fn().mockResolvedValue([]),
+ },
+ } as unknown as PrismaService;
+}
+
+function fakeProtocolParamsService(): ProtocolParamsService {
+ return {
+ snapshotForChain: jest.fn().mockReturnValue({
+ version: 0,
+ feeBps: 30,
+ deadlineSeconds: 1800,
+ fillWindowSeconds: 600,
+ capturedAt: new Date().toISOString(),
+ }),
+ getCurrent: jest.fn().mockReturnValue({ version: 0, feeBps: 30, chains: {}, maxExposureRatio: 0.05, slashAmount: "100000000", activeSinceLedger: 0, adoptedAt: new Date().toISOString() }),
+ getPending: jest.fn().mockReturnValue(null),
+ getHistory: jest.fn().mockReturnValue([]),
+ } as unknown as ProtocolParamsService;
+}
+
+function makeService(
+ configOverrides: { onchainIntentsEnabled?: boolean; settlementContractId?: string } = {},
+ stellarTx?: jest.Mocked,
+) {
+ return new IntentsService(
+ new InMemoryIntentsRepository(),
+ fakeConfig(configOverrides),
+ stellarTx ?? fakeStellarTxService(),
+ fakePrismaService(),
+ fakeProtocolParamsService(),
+ );
+}
+
+function validCreateData() {
+ return {
+ user: Keypair.random().publicKey(),
+ srcChain: "ethereum" as const,
+ srcToken: { address: "0xabc", symbol: "USDC", name: "USD Coin", decimals: 6, chain: "ethereum" as const },
+ srcAmount: "1000000",
+ dstToken: { contract: VALID_CONTRACT_ID, symbol: "USDC", decimals: 7 },
+ minDstAmount: "990000",
+ deadline: Math.floor(Date.now() / 1000) + 1800,
+ };
+}
+
+async function buildService(
+ configOverrides: { onchainIntentsEnabled?: boolean; settlementContractId?: string } = {},
+ stellarTxService?: jest.Mocked,
+): Promise {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ {
+ provide: INTENTS_REPOSITORY,
+ useClass: InMemoryIntentsRepository,
+ },
+ {
+ provide: ConfigService,
+ useValue: fakeConfig(configOverrides),
+ },
+ {
+ provide: StellarTxService,
+ useValue: stellarTxService ?? fakeStellarTxService(),
+ },
+ {
+ provide: PrismaService,
+ useValue: fakePrismaService(),
+ },
+ {
+ provide: ProtocolParamsService,
+ useValue: fakeProtocolParamsService(),
+ },
+ IntentsService,
+ ],
+ }).compile();
+
+ return module.get(IntentsService);
+}
+
+describe("IntentsService", () => {
+ let service: IntentsService;
+
+ beforeEach(() => {
+ service = makeService();
+ });
+
+ it("seeds 5 intents on construction", async () => {
+ expect(await service.getAll()).toHaveLength(5);
+ });
+
+ it("getAll returns intents sorted by createdAt descending", async () => {
+ const all = await service.getAll();
+ for (let i = 1; i < all.length; i++) {
+ expect(all[i - 1].createdAt).toBeGreaterThanOrEqual(all[i].createdAt);
+ }
+ });
+
+ it("create adds an open intent with a generated id", async () => {
+ const before = (await service.getAll()).length;
+ const deadline = Math.floor(Date.now() / 1000) + 1800;
+ const intent = await service.create({
+ user: "GTEST...0000",
+ srcChain: "ethereum",
+ srcToken: { address: "0xabc", symbol: "USDC", name: "USD Coin", decimals: 6, chain: "ethereum" },
+ srcAmount: "1000000",
+ dstToken: { contract: "CTEST", symbol: "USDC", decimals: 7 },
+ minDstAmount: "990000",
+ deadline,
+ });
+
+ expect(intent.state).toBe("open");
+ expect(intent.intentId).toBeTruthy();
+ expect(intent.deadline).toBe(deadline);
+ expect(await service.getAll()).toHaveLength(before + 1);
+ });
+
+ it("create defaults deadline to now + 1800 when omitted", async () => {
+ const before = Math.floor(Date.now() / 1000);
+ const intent = await service.create({
+ user: "GTEST...0000",
+ srcChain: "ethereum",
+ srcToken: { address: "0xabc", symbol: "USDC", name: "USD Coin", decimals: 6, chain: "ethereum" },
+ srcAmount: "1000000",
+ dstToken: { contract: "CTEST", symbol: "USDC", decimals: 7 },
+ minDstAmount: "990000",
+ deadline: undefined as unknown as number,
+ });
+
+ expect(intent.deadline).toBeGreaterThanOrEqual(before + 1800);
+ });
+
+ it("get returns undefined for an unknown id", async () => {
+ expect(await service.get("does-not-exist")).toBeUndefined();
+ });
+
+ it("update mutates and returns the patched intent", async () => {
+ const [existing] = await service.getByState("open");
+ const updated = await service.update(existing.intentId, { state: "accepted", solver: "SOLVER_X" });
+
+ expect(updated?.state).toBe("accepted");
+ expect(updated?.solver).toBe("SOLVER_X");
+ expect((await service.get(existing.intentId))?.state).toBe("accepted");
+ });
+
+ it("update returns null for an unknown id", async () => {
+ expect(await service.update("does-not-exist", { state: "cancelled" })).toBeNull();
+ });
+
+ it("getByUser is case-insensitive", async () => {
+ const [existing] = await service.getAll();
+ const found = await service.getByUser(existing.user.toLowerCase());
+ expect(found.some((i) => i.intentId === existing.intentId)).toBe(true);
+ });
+
+ it("getByState only returns intents in that state", async () => {
+ for (const intent of await service.getByState("filled")) {
+ expect(intent.state).toBe("filled");
+ }
+ });
+
+ describe("acceptIfOpen", () => {
+ it("transitions an open intent to accepted and returns it", async () => {
+ const [open] = await service.getByState("open");
+ const result = await service.acceptIfOpen(open.intentId, "SOLVER_X");
+
+ expect(result).not.toBeNull();
+ expect(result!.state).toBe("accepted");
+ expect(result!.solver).toBe("SOLVER_X");
+ expect((await service.get(open.intentId))!.state).toBe("accepted");
+ });
+
+ it("returns null for a non-existent intent", async () => {
+ expect(await service.acceptIfOpen("does-not-exist", "SOLVER_X")).toBeNull();
+ });
+
+ it("returns null when the intent is already accepted", async () => {
+ const [accepted] = await service.getByState("accepted");
+ expect(await service.acceptIfOpen(accepted.intentId, "SOLVER_X")).toBeNull();
+ });
+
+ it("only the first caller wins under simulated concurrency", async () => {
+ const [open] = await service.getByState("open");
+ const results = await Promise.all(
+ Array.from({ length: 10 }, (_, i) =>
+ service.acceptIfOpen(open.intentId, `SOLVER_${i}`),
+ ),
+ );
+
+ const successes = results.filter((r) => r !== null);
+ expect(successes).toHaveLength(1);
+ expect(successes[0]!.state).toBe("accepted");
+ });
+
+ // -----------------------------------------------------------------------
+ // Per-chain fill-window tests (issue: chain-aware fill window)
+ // -----------------------------------------------------------------------
+
+ it("sets deadline to now + stellar fill window (120 s) for a stellar intent", async () => {
+ const now = Math.floor(Date.now() / 1000);
+ const intent = await service.create({
+ user: "GTEST_STELLAR_CHAIN1",
+ srcChain: "stellar",
+ srcToken: { address: "native", symbol: "XLM", name: "Stellar Lumens", decimals: 7, chain: "stellar" },
+ srcAmount: "1000000",
+ dstToken: { contract: "CTEST", symbol: "USDC", decimals: 7 },
+ minDstAmount: "990000",
+ deadline: now + 900,
+ });
+
+ const result = await service.acceptIfOpen(intent.intentId, "SOLVER_X");
+
+ expect(result).not.toBeNull();
+ const expectedWindow = CHAIN_FILL_WINDOW_DEFAULTS["stellar"] ?? DEFAULT_FILL_WINDOW_SECONDS;
+ // Allow a 2-second tolerance for test execution time
+ expect(result!.deadline).toBeGreaterThanOrEqual(now + expectedWindow - 2);
+ expect(result!.deadline).toBeLessThanOrEqual(now + expectedWindow + 2);
+ });
+
+ it("sets deadline to now + ethereum fill window (1800 s) for an ethereum intent", async () => {
+ const now = Math.floor(Date.now() / 1000);
+ const intent = await service.create({
+ user: "GTEST_ETHEREUM_CHAIN1",
+ srcChain: "ethereum",
+ srcToken: { address: "0xabc", symbol: "USDC", name: "USD Coin", decimals: 6, chain: "ethereum" },
+ srcAmount: "1000000",
+ dstToken: { contract: "CTEST", symbol: "USDC", decimals: 7 },
+ minDstAmount: "990000",
+ deadline: now + 3600,
+ });
+
+ const result = await service.acceptIfOpen(intent.intentId, "SOLVER_X");
+
+ expect(result).not.toBeNull();
+ const expectedWindow = CHAIN_FILL_WINDOW_DEFAULTS["ethereum"] ?? DEFAULT_FILL_WINDOW_SECONDS;
+ // Allow a 2-second tolerance for test execution time
+ expect(result!.deadline).toBeGreaterThanOrEqual(now + expectedWindow - 2);
+ expect(result!.deadline).toBeLessThanOrEqual(now + expectedWindow + 2);
+ });
+
+ it("stellar and ethereum accepted intents get distinct (non-equal) fill deadlines", async () => {
+ const now = Math.floor(Date.now() / 1000);
+
+ const stellarIntent = await service.create({
+ user: "GTEST_STELLAR_DIFF1",
+ srcChain: "stellar",
+ srcToken: { address: "native", symbol: "XLM", name: "Stellar Lumens", decimals: 7, chain: "stellar" },
+ srcAmount: "1000000",
+ dstToken: { contract: "CTEST", symbol: "USDC", decimals: 7 },
+ minDstAmount: "990000",
+ deadline: now + 900,
+ });
+ const ethIntent = await service.create({
+ user: "GTEST_ETHEREUM_DIFF1",
+ srcChain: "ethereum",
+ srcToken: { address: "0xabc", symbol: "USDC", name: "USD Coin", decimals: 6, chain: "ethereum" },
+ srcAmount: "1000000",
+ dstToken: { contract: "CTEST", symbol: "USDC", decimals: 7 },
+ minDstAmount: "990000",
+ deadline: now + 3600,
+ });
+
+ const stellarResult = await service.acceptIfOpen(stellarIntent.intentId, "SOLVER_STELLAR");
+ const ethResult = await service.acceptIfOpen(ethIntent.intentId, "SOLVER_ETH");
+
+ expect(stellarResult).not.toBeNull();
+ expect(ethResult).not.toBeNull();
+
+ // Ethereum solver gets a materially larger fill window than Stellar
+ expect(ethResult!.deadline).toBeGreaterThan(stellarResult!.deadline);
+
+ // Confirm the windows match the config constants exactly (allowing 2 s clock drift)
+ const stellarWindow = CHAIN_FILL_WINDOW_DEFAULTS["stellar"] ?? DEFAULT_FILL_WINDOW_SECONDS;
+ const ethWindow = CHAIN_FILL_WINDOW_DEFAULTS["ethereum"] ?? DEFAULT_FILL_WINDOW_SECONDS;
+ expect(ethWindow).toBeGreaterThan(stellarWindow); // sanity-check on config
+ });
+
+ it("falls back to DEFAULT_FILL_WINDOW_SECONDS for an unknown chain", async () => {
+ const now = Math.floor(Date.now() / 1000);
+ const intent = await service.create({
+ user: "GTEST_UNKNOWN_CHAIN01",
+ srcChain: "stellar", // create as valid chain, then patch for test
+ srcToken: { address: "native", symbol: "XLM", name: "Stellar Lumens", decimals: 7, chain: "stellar" },
+ srcAmount: "1000000",
+ dstToken: { contract: "CTEST", symbol: "USDC", decimals: 7 },
+ minDstAmount: "990000",
+ deadline: now + 3600,
+ });
+ // Manually patch to an unknown chain to exercise the fallback
+ await service.update(intent.intentId, { srcChain: "unknown_chain" as never });
+
+ const result = await service.acceptIfOpen(intent.intentId, "SOLVER_X");
+
+ expect(result).not.toBeNull();
+ expect(result!.deadline).toBeGreaterThanOrEqual(now + DEFAULT_FILL_WINDOW_SECONDS - 2);
+ expect(result!.deadline).toBeLessThanOrEqual(now + DEFAULT_FILL_WINDOW_SECONDS + 2);
+ });
+ }); // end describe("acceptIfOpen")
+
+ describe("fillIfAccepted", () => {
+ it("transitions an accepted intent to filled when solver matches", async () => {
+ const [accepted] = await service.getByState("accepted");
+ const result = await service.fillIfAccepted(accepted.intentId, accepted.solver!, {
+ fillAmount: "100",
+ txHash: "test-hash",
+ filledAt: Math.floor(Date.now() / 1000),
+ });
+
+ expect(result).not.toBeNull();
+ expect(result!.state).toBe("filled");
+ expect(result!.fillAmount).toBe("100");
+ });
+
+ it("returns null when solver does not match", async () => {
+ const [accepted] = await service.getByState("accepted");
+ const result = await service.fillIfAccepted(accepted.intentId, "WRONG_SOLVER", {
+ fillAmount: "100",
+ });
+ expect(result).toBeNull();
+ });
+
+ it("returns null for a non-existent intent", async () => {
+ expect(await service.fillIfAccepted("nope", "SOLVER_X", {})).toBeNull();
+ });
+
+ it("only the first caller wins under simulated concurrency", async () => {
+ const [accepted] = await service.getByState("accepted");
+ const results = await Promise.all(
+ Array.from({ length: 10 }, () =>
+ service.fillIfAccepted(accepted.intentId, accepted.solver!, {
+ fillAmount: "100",
+ txHash: "race-hash",
+ filledAt: Math.floor(Date.now() / 1000),
+ }),
+ ),
+ );
+
+ const successes = results.filter((r) => r !== null);
+ expect(successes).toHaveLength(1);
+ expect(successes[0]!.state).toBe("filled");
+ });
+ });
+
+ describe("on-chain registration (ONCHAIN_INTENTS_ENABLED)", () => {
+ it("stays fully in the repository when the flag is off, never touching StellarTxService", async () => {
+ const stellarTxService = fakeStellarTxService();
+ const svc = makeService({ onchainIntentsEnabled: false }, stellarTxService);
+
+ const intent = await svc.create(validCreateData());
+
+ expect(stellarTxService.invokeContract).not.toHaveBeenCalled();
+ expect(await svc.get(intent.intentId)).toEqual(intent);
+ });
+
+ it("invokes the settlement contract and preserves the Intent shape when the flag is on", async () => {
+ const stellarTxService = fakeStellarTxService();
+ stellarTxService.invokeContract.mockResolvedValue({ hash: "deadbeef", status: "SUCCESS" } as never);
+ const svc = makeService(
+ { onchainIntentsEnabled: true, settlementContractId: VALID_CONTRACT_ID },
+ stellarTxService,
+ );
+
+ const data = validCreateData();
+ const intent = await svc.create(data);
+
+ expect(stellarTxService.invokeContract).toHaveBeenCalledTimes(1);
+ const call = stellarTxService.invokeContract.mock.calls[0][0];
+ expect(call.contractId).toBe(VALID_CONTRACT_ID);
+ expect(call.method).toBe("create_intent");
+
+ // Response shape: the in-memory and on-chain paths return the same keys.
+ // usdValueAtCreate (#440) and paramsVersion (#500) are both stamped by
+ // persistNewIntent, so they appear on every newly created intent.
+ expect(Object.keys(intent).sort()).toEqual(
+ Object.keys({
+ intentId: "",
+ user: "",
+ srcChain: "",
+ srcToken: "",
+ srcAmount: "",
+ dstToken: "",
+ minDstAmount: "",
+ state: "",
+ createdAt: 0,
+ deadline: 0,
+ paramsVersion: 0,
+ usdValueAtCreate: 0,
+ // Issue #385: stamped on every create (set on the onchain path,
+ // undefined otherwise) so both paths return the same shape.
+ pendingTxHash: "",
+ pendingOp: "",
+ // Issue #403: stamped on every create so the deposit-verification
+ // loop can distinguish pending intents from skipped/disabled ones.
+ srcVerified: false,
+ srcVerification: "",
+ }).sort(),
+ );
+ expect(await svc.get(intent.intentId)).toBeDefined();
+ });
+
+ it("rejects with a clear error and does not create the intent when SETTLEMENT_CONTRACT_ID is unset", async () => {
+ const stellarTxService = fakeStellarTxService();
+ const service = makeService({ onchainIntentsEnabled: true }, stellarTxService);
+ const before = (await service.getAll()).length;
+
+ await expect(service.create(validCreateData())).rejects.toMatchObject({
+ message: expect.stringContaining("SETTLEMENT_CONTRACT_ID"),
+ });
+ expect(stellarTxService.invokeContract).not.toHaveBeenCalled();
+ expect(await service.getAll()).toHaveLength(before);
+ });
+
+ it("rejects and does not create the intent when the on-chain call fails", async () => {
+ const stellarTxService = fakeStellarTxService();
+ stellarTxService.invokeContract.mockRejectedValue(new Error("submission failed after 5 attempts"));
+ const svc = makeService(
+ { onchainIntentsEnabled: true, settlementContractId: VALID_CONTRACT_ID },
+ stellarTxService,
+ );
+ const before = (await svc.getAll()).length;
+
+ await expect(svc.create(validCreateData())).rejects.toThrow(/settlement contract/i);
+ expect(await svc.getAll()).toHaveLength(before);
+ });
+ });
+
+ // ---------------------------------------------------------------------------
+ // Audit trail (issue #217 / #62)
+ // ---------------------------------------------------------------------------
+
+ describe("appendAuditEntry / getAuditLog", () => {
+ it("returns an empty array for an intent with no audit entries", () => {
+ expect(service.getAuditLog("no-such-intent")).toEqual([]);
+ });
+
+ it("appends a single entry and getAuditLog returns it", () => {
+ service.appendAuditEntry("intent-1", "cancelled", "USER_ADDR", "user cancelled");
+ const log = service.getAuditLog("intent-1");
+ expect(log).toHaveLength(1);
+ expect(log[0]).toMatchObject({
+ toState: "cancelled",
+ actor: "USER_ADDR",
+ reason: "user cancelled",
+ });
+ expect(log[0].timestamp).toBeTruthy(); // ISO timestamp
+ });
+
+ it("appends multiple entries in order and getAuditLog returns oldest-first", async () => {
+ service.appendAuditEntry("intent-2", "accepted", "SOLVER_A", "solver accepted");
+ await new Promise((r) => setTimeout(r, 5)); // small gap so timestamps differ
+ service.appendAuditEntry("intent-2", "filled", "SOLVER_A", "solver filled");
+
+ const log = service.getAuditLog("intent-2");
+ expect(log).toHaveLength(2);
+ expect(log[0].toState).toBe("accepted");
+ expect(log[1].toState).toBe("filled");
+ });
+
+ it("stores optional metadata in the entry", () => {
+ service.appendAuditEntry("intent-3", "expired", "system", "deadline passed", {
+ deadline: 1234567890,
+ sweepedAt: 1234567900,
+ });
+ const log = service.getAuditLog("intent-3");
+ expect(log[0].metadata).toEqual({ deadline: 1234567890, sweepedAt: 1234567900 });
+ });
+
+ it("does not mix entries across different intentIds", () => {
+ service.appendAuditEntry("intent-A", "cancelled", "USER_A", "cancel A");
+ service.appendAuditEntry("intent-B", "expired", "system", "expire B");
+
+ expect(service.getAuditLog("intent-A")).toHaveLength(1);
+ expect(service.getAuditLog("intent-B")).toHaveLength(1);
+ expect(service.getAuditLog("intent-A")[0].toState).toBe("cancelled");
+ expect(service.getAuditLog("intent-B")[0].toState).toBe("expired");
+ });
+
+ it("fires a DB write via PrismaService on each append (non-blocking)", async () => {
+ const prismaService = {
+ intentAuditLog: {
+ create: jest.fn().mockResolvedValue({}),
+ findMany: jest.fn().mockResolvedValue([]),
+ },
+ } as unknown as PrismaService;
+ const svc = new IntentsService(new InMemoryIntentsRepository(), fakeConfig(), fakeStellarTxService(), prismaService, fakeProtocolParamsService());
+
+ svc.appendAuditEntry("intent-db", "slashed", "system", "missed fill", { foo: "bar" });
+
+ // The DB write is fire-and-forget — wait one tick for the promise chain
+ await new Promise((r) => setImmediate(r));
+
+ const mockPrisma = prismaService as unknown as {
+ intentAuditLog: { create: jest.Mock };
+ };
+ expect(mockPrisma.intentAuditLog.create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ intentId: "intent-db",
+ toState: "slashed",
+ actor: "system",
+ reason: "missed fill",
+ }),
+ }),
+ );
+ });
+
+ it("does NOT throw when the DB write fails — logs an error but returns normally", async () => {
+ const prismaService = {
+ intentAuditLog: {
+ create: jest.fn().mockRejectedValue(new Error("DB is down")),
+ findMany: jest.fn().mockResolvedValue([]),
+ },
+ } as unknown as PrismaService;
+ const svc = new IntentsService(new InMemoryIntentsRepository(), fakeConfig(), fakeStellarTxService(), prismaService, fakeProtocolParamsService());
+
+ // Should not throw synchronously
+ expect(() =>
+ svc.appendAuditEntry("intent-fail", "expired", "system", "deadline"),
+ ).not.toThrow();
+
+ // In-memory log still has the entry
+ expect(svc.getAuditLog("intent-fail")).toHaveLength(1);
+
+ // Wait for the rejected promise — should not propagate
+ await new Promise((r) => setImmediate(r));
+ // No unhandled rejection here (jest would fail the test if one occurred)
+ });
+ });
+});
diff --git a/src/intents/intents.service.ts b/src/intents/intents.service.ts
index fd81a2af..d06733e2 100644
--- a/src/intents/intents.service.ts
+++ b/src/intents/intents.service.ts
@@ -8,8 +8,12 @@ import {
import { ConfigService } from "@nestjs/config";
import { v4 as uuidv4 } from "uuid";
import { Address, nativeToScVal, xdr } from "@stellar/stellar-sdk";
-import { Intent, IntentAuditEntry, IntentState } from "./intents.types";
-import { INTENTS_REPOSITORY, IIntentsRepository } from "./intents.repository";
+import { Intent, IntentAuditEntry, IntentState, PendingIntentOp } from "./intents.types";
+import { INTENTS_REPOSITORY, IIntentsRepository, isVersionConflict, MutationResult } from "./intents.repository";
+import { INTENTS_UNIT_OF_WORK, IIntentsUnitOfWork } from "./intents.unit-of-work";
+import { IntentDeadlineScheduler } from "./intents-deadline.jobs";
+import { IOutboxWriter, NewOutboxEntry } from "../soroban/outbox.repository";
+import { buildOutboxInvocation, createIntentEntry } from "../soroban/outbox-operations";
import { AppConfig } from "../config/configuration";
import {
CHAIN_DEADLINE_DEFAULTS,
@@ -28,7 +32,9 @@ import { FeatureFlagService } from "../flags/feature-flag.service";
const TERMINAL_STATES: IntentState[] = ["filled", "cancelled", "expired", "slashed"];
/**
- * Sentinel `from_state` for the transition into "open".
+ * Sentinel `from_state` for the transition at intent creation — into `open`
+ * on the in-memory path, or into `pending_open` when the onchain rollout
+ * parks the fresh intent (issue #385).
*
* Not an {@link IntentState}: creation has no prior state, and inventing one
* would put a value in the `from_state` label that no lifecycle edge can
@@ -37,6 +43,34 @@ const TERMINAL_STATES: IntentState[] = ["filled", "cancelled", "expired", "slash
*/
const NONE_STATE = "none";
+/** pending_* state each chain write parks in (issue #385). */
+const PENDING_STATE_BY_OP: Record = {
+ create: "pending_open",
+ accept: "pending_accepted",
+ fill: "pending_filled",
+ cancel: "pending_cancelled",
+};
+
+/** Confirmed base state each pending_* marker settles back to (issue #385). */
+const CONFIRMED_STATE_BY_PENDING: Partial> = {
+ pending_open: "open",
+ pending_accepted: "accepted",
+ pending_filled: "filled",
+ pending_cancelled: "cancelled",
+};
+
+/**
+ * Settlement-contract method each write broadcasts (issue #385). Names mirror
+ * the shadow hooks (`observeAccept`/`observeFill`/`cancelIfOpen`) so the
+ * simulated and the real invocation stay the same call.
+ */
+const ONCHAIN_METHOD_BY_OP: Record = {
+ create: "create_intent",
+ accept: "accept_intent",
+ fill: "fill_intent",
+ cancel: "cancel_intent",
+};
+
/**
* Runtime check that `transition` is one of the five the shadow monitor models.
*
@@ -51,6 +85,43 @@ function isKnownShadowTransition(transition: ShadowTransition): boolean {
/** How long a completed idempotency-key result stays replayable. */
const IDEMPOTENCY_TTL_SECONDS = 86_400; // 24 hours
+/**
+ * Compute the USD value of a base-unit amount at a given token price (issue #440).
+ *
+ * Uses integer arithmetic for the amount (BigInt) so large base-unit values do
+ * not lose precision before the float conversion; the price is scaled to 1e8
+ * to keep the multiplication in integer space. Returns `undefined` when the
+ * price is unknown — historical rows are never backfilled with fabricated
+ * values.
+ */
+function computeUsdValue(
+ srcAmount: string,
+ decimals: number,
+ priceUsd: number | undefined,
+): number | undefined {
+ if (priceUsd === undefined || priceUsd === null || !Number.isFinite(priceUsd)) return undefined;
+ try {
+ const amount = BigInt(srcAmount);
+ const scale = 10n ** BigInt(decimals);
+ const scaled = amount * BigInt(Math.round(priceUsd * 1e8));
+ return Number(scaled / (scale * 100_000_000n));
+ } catch {
+ return undefined;
+ }
+}
+
+/**
+ * Narrow an optimistic-concurrency mutation result to the written record.
+ *
+ * `VersionConflict` means a concurrent writer won the race — the guarded
+ * transition did not happen — which every service-level caller treats exactly
+ * like "no match": `null`. The conflict details remain available to callers
+ * that talk to the repository directly (see `etag.ts` / `preconditionFailed`).
+ */
+function written(result: MutationResult): Intent | null {
+ return result !== null && !isVersionConflict(result) ? result : null;
+}
+
/**
* Maximum number of simultaneously open (state = "open" | "accepted") intents
* allowed per user address.
@@ -68,6 +139,13 @@ const IDEMPOTENCY_TTL_SECONDS = 86_400; // 24 hours
*/
export const MAX_OPEN_INTENTS_PER_USER = 50;
+/**
+ * Upper bound on re-read → retry attempts inside {@link IntentsService.mutateWithRetry}.
+ * Three conflicts in a row means a hot row; giving up keeps a retrying writer
+ * from starving concurrent readers (issue #405).
+ */
+export const MAX_VERSION_RETRIES = 3;
+
/** Payload for creating a new intent. */
export type NewIntentData = Omit;
@@ -133,8 +211,38 @@ export class IntentsService {
*/
@Optional() private readonly metricsService?: MetricsService,
@Optional() private readonly flags?: FeatureFlagService,
+ /**
+ * Transactional outbox unit of work (issue #396).
+ *
+ * Present only where the durable outbox is wired (and in the crash-injection
+ * harnesses): while it is injected, `create()` commits the intent row and its
+ * `create_intent` outbox row atomically and the relay submits later, instead
+ * of calling the settlement contract inline. Absent — the default graph at
+ * HEAD — creation keeps the direct, synchronous registration path.
+ */
+ @Optional() @Inject(INTENTS_UNIT_OF_WORK)
+ private readonly unitOfWork?: IIntentsUnitOfWork,
+ /**
+ * Deadline-job scheduler (issue #550). Registers the expire/fill-window
+ * wake-ups whenever an intent is created or its fill window starts, so the
+ * sweeper settles deadlines through the jobs queue instead of polling.
+ * `@Optional()` because unit harnesses that never exercise deadline jobs
+ * construct this service directly without one.
+ */
+ @Optional() private readonly deadlines?: IntentDeadlineScheduler,
) {}
+ /**
+ * Lifecycle hook (Nest `OnModuleDestroy`): drops the in-memory idempotency
+ * caches. This service owns no timers — deadline wake-ups live in the jobs
+ * queue — so the caches are the only teardown the instance has. Called
+ * directly by the unit harnesses after each case for the same reason.
+ */
+ onModuleDestroy(): void {
+ this.idempotencyCache.clear();
+ this.idempotencyInFlight.clear();
+ }
+
/**
* Logs the store size and evicts stale terminal intents from the in-memory
* adapter when it is the active backend. This keeps the memory footprint
@@ -287,6 +395,41 @@ export class IntentsService {
* Build, optionally register on-chain, and persist a brand-new intent.
* Contains no idempotency logic — deduplication is the caller's concern.
*/
+ /**
+ * Creation-time source-deposit-verification verdict (issue #403).
+ *
+ * - `evm.depositVerificationEnabled=false` → everything is marked verified
+ * up front (`skipped`) so the verify loop never picks it up.
+ * - Non-EVM source chains (Stellar) are out of the EVM verifier's scope →
+ * `skipped`, verified.
+ * - Otherwise the intent is born `pending` / unverified until the
+ * source-deposit verification service ticks it.
+ */
+ private initialSrcVerification(
+ srcChain: NewIntentData["srcChain"],
+ now: number,
+ ): Pick {
+ const enabled =
+ this.configService.get("evm", { infer: true })?.depositVerificationEnabled === true;
+ if (!enabled) {
+ return {
+ srcVerified: true,
+ srcVerification: {
+ status: "skipped",
+ checkedAt: now,
+ detail: "deposit verification disabled",
+ },
+ };
+ }
+ if (srcChain === "stellar") {
+ return {
+ srcVerified: true,
+ srcVerification: { status: "skipped", checkedAt: now, detail: "non-EVM source chain" },
+ };
+ }
+ return { srcVerified: false, srcVerification: { status: "pending", checkedAt: now } };
+ }
+
private async persistNewIntent(
data: Omit,
): Promise {
@@ -299,41 +442,113 @@ export class IntentsService {
const defaultDeadline = data.deadline ?? now + paramsSnapshot.deadlineSeconds;
const intent: Intent = {
+ // Issue #403: stamp the creation-time source-deposit verdict first so
+ // the row is self-describing from its first write — the verify loop
+ // picks intents up by `srcVerified === false` and never re-checks
+ // "skipped"/"grandfathered" ones. An explicit caller-supplied verdict
+ // (imports, seeds) wins over this default.
+ ...this.initialSrcVerification(data.srcChain, now),
...data,
intentId: uuidv4(),
state: "open",
createdAt: now,
deadline: defaultDeadline,
paramsVersion: paramsSnapshot.version,
+ usdValueAtCreate: computeUsdValue(
+ data.srcAmount,
+ data.srcToken.decimals,
+ data.srcToken.priceUSD,
+ ),
+ // Issue #385: both creation paths carry the pending keys — undefined
+ // until a chain write is in flight — so the in-memory and on-chain
+ // paths return an identical shape.
+ pendingTxHash: undefined,
+ pendingOp: undefined,
};
// ONCHAIN_INTENTS_ENABLED is the default; the `onchain-intents-enabled`
// runtime flag (issue #495) can roll it out per chain / percentage.
- const onchain = this.flags
- ? await this.flags.getBooleanValue("onchain-intents-enabled", {
- targetingKey: intent.intentId,
- chain: intent.srcChain,
- })
- : this.configService.get("onchainIntentsEnabled", { infer: true });
- if (onchain) {
- await this.registerOnChain(intent);
+ const onchain = await this.isOnchainWrite(intent);
+ if (onchain && this.unitOfWork) {
+ // Issue #396: intent row + create_intent outbox row commit atomically;
+ // OutboxRelayService submits afterwards, never inside this request.
+ // Issue #385: nothing has reached the chain yet, so the row is born
+ // `pending_open` — `pendingTxHash` is attached once the confirmation
+ // watcher observes the relay's transaction.
+ intent.state = "pending_open";
+ intent.pendingOp = "create";
+ await this.unitOfWork.run(async ({ intents, outbox }) => {
+ await this.enqueueOnchain(outbox, intent);
+ await intents.save(intent);
+ });
+ } else if (onchain) {
+ // Registration must land before the row exists: a failed broadcast
+ // rejects the whole creation, leaving no dangling `pending_open` record
+ // behind (issue #385).
+ const txHash = await this.registerOnChain(intent);
+ intent.state = "pending_open";
+ intent.pendingOp = "create";
+ intent.pendingTxHash = txHash;
+ await this.repo.save(intent);
+ } else {
+ await this.repo.save(intent);
}
-
- await this.repo.save(intent);
+ // Issue #550: wake the sweeper at this intent's deadline through the jobs
+ // queue instead of relying on the legacy 30s poll.
+ this.deadlines?.scheduleExpire(intent);
// Creation is the entry edge of the funnel: the `vortex:intent:*` recording
// rules count transitions *into* each state, so without this the intent
// dashboard would start every conversion ratio from zero. `from_state` is
// the sentinel "none" — an intent that does not exist yet has no state.
- this.countTransition(NONE_STATE, "open");
+ this.countTransition(NONE_STATE, intent.state);
return intent;
}
/**
- * Registers `intent` with the settlement contract. Only called when
- * ONCHAIN_INTENTS_ENABLED is on; while that flag is off, create() stays
- * fully in-memory (the rollout fallback).
+ * Queues the settlement contract's `create_intent` call for `intent` on the
+ * outbox (issue #396), for the unit-of-work path of {@link persistNewIntent}.
+ * Nothing is broadcast here — the relay submits afterwards, so the request
+ * never waits on Soroban and an Soroban outage cannot fail intent creation.
*/
- private async registerOnChain(intent: Intent): Promise {
+ private async enqueueOnchain(outbox: IOutboxWriter, intent: Intent): Promise {
+ const contractId = this.configService.get("stellar.settlementContractId", { infer: true });
+ if (!contractId) {
+ throw new ServiceUnavailableException(
+ "On-chain intent registration is enabled but SETTLEMENT_CONTRACT_ID is not configured",
+ );
+ }
+ const entry = createIntentEntry(intent);
+ // Validates the entry encodes to a contract call before it can become a
+ // poison row.
+ buildOutboxInvocation(entry, contractId);
+ await outbox.enqueue(entry);
+ this.logger.log(`Queued on-chain registration for intent ${intent.intentId}`);
+ }
+
+ /**
+ * Does a write for `intent` need the settlement contract? Resolved from the
+ * runtime rollout flag when the flag service is wired (issue #495), from
+ * `ONCHAIN_INTENTS_ENABLED` otherwise — shared by creation and the later
+ * accept/fill/cancel writes so they can never disagree about a rollout.
+ */
+ private async isOnchainWrite(intent: Intent): Promise {
+ if (this.flags) {
+ return this.flags.getBooleanValue("onchain-intents-enabled", {
+ targetingKey: intent.intentId,
+ chain: intent.srcChain,
+ });
+ }
+ return this.configService.get("onchainIntentsEnabled", { infer: true });
+ }
+
+ /**
+ * Registers `intent` with the settlement contract, returning the broadcast
+ * transaction hash (issue #385 — stamped as `pendingTxHash`). Only called
+ * when ONCHAIN_INTENTS_ENABLED is on and no outbox unit of work is wired;
+ * while that flag is off, create() stays fully in-memory (the rollout
+ * fallback).
+ */
+ private async registerOnChain(intent: Intent): Promise {
const contractId = this.configService.get("stellar.settlementContractId", { infer: true });
if (!contractId) {
throw new ServiceUnavailableException(
@@ -348,6 +563,7 @@ export class IntentsService {
args: this.buildCreateIntentArgs(intent),
});
this.logger.log(`Registered intent ${intent.intentId} on-chain (tx ${result.hash})`);
+ return result.hash;
} catch (err) {
this.logger.error(
`Failed to register intent ${intent.intentId} on-chain: ${(err as Error).message}`,
@@ -513,7 +729,10 @@ export class IntentsService {
}
/**
- * Count the number of intents in "open" or "accepted" state for a user.
+ * Count the number of intents standing for a user: `open` or `accepted`,
+ * plus their `pending_open` / `pending_accepted` in-flight variants
+ * (issue #385) — a chain write that has not confirmed yet still occupies
+ * the user's {@link MAX_OPEN_INTENTS_PER_USER} slot.
*
* Used by IntentsController.create() to enforce MAX_OPEN_INTENTS_PER_USER.
* The query is a simple filter over findByUser so it works identically
@@ -524,7 +743,11 @@ export class IntentsService {
async countOpenByUser(user: string): Promise {
const userIntents = await this.repo.findByUser(user);
return userIntents.filter(
- (i) => i.state === "open" || i.state === "accepted",
+ (i) =>
+ i.state === "open" ||
+ i.state === "accepted" ||
+ i.state === "pending_open" ||
+ i.state === "pending_accepted",
).length;
}
@@ -538,14 +761,44 @@ export class IntentsService {
* it is used by test setup only. It is logged so that a future production
* caller is caught in review rather than silently skewing the dashboards.
*/
- async update(id: string, patch: Partial): Promise {
+ async update(
+ id: string,
+ patch: Partial,
+ expectedVersion?: number,
+ ): Promise {
if (patch.state !== undefined) {
this.logger.warn(
`[state-machine] update(${id}) carries a state patch ("${patch.state}"); ` +
`this bypasses the guarded transitions and their observers`,
);
}
- return this.repo.update(id, patch);
+ return written(await this.repo.update(id, patch, expectedVersion));
+ }
+
+ /**
+ * Re-read → mutate loop for writers for whom retrying is semantically safe
+ * (the sweeper, quote persistence, deposit verification). `mutate` receives
+ * the freshly-read intent and returns the versioned mutation to attempt, or
+ * `undefined` when the intent no longer needs changing — which ends the loop
+ * with `null`. Bounded by {@link MAX_VERSION_RETRIES}; if every attempt
+ * conflicts, the last VersionConflict is returned.
+ */
+ async mutateWithRetry(
+ id: string,
+ mutate: (current: Intent) => Promise | MutationResult | undefined,
+ maxAttempts = MAX_VERSION_RETRIES,
+ ): Promise {
+ let last: MutationResult = null;
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ const current = await this.repo.findById(id);
+ if (!current) return null;
+ const pending = mutate(current);
+ if (pending === undefined) return null;
+ last = await pending;
+ if (!isVersionConflict(last)) return last;
+ }
+ this.logger.warn(`[occ] gave up on intent ${id} after ${maxAttempts} version conflicts`);
+ return last;
}
/** Amend an open intent without changing its ID or creation history. */
@@ -574,11 +827,26 @@ export class IntentsService {
const nowSec = now ?? Math.floor(Date.now() / 1000);
const fillWindow =
CHAIN_FILL_WINDOW_DEFAULTS[intent.srcChain] ?? DEFAULT_FILL_WINDOW_SECONDS;
- const updated = await this.repo.acceptIfOpen(id, solver, nowSec + fillWindow, nowSec);
- if (updated !== null) this.countTransition("open", "accepted");
+ const updated = written(await this.repo.acceptIfOpen(id, solver, nowSec + fillWindow, nowSec));
+ if (updated !== null) {
+ this.countTransition("open", "accepted");
+ // Issue #550: the fill window is now the binding deadline — schedule the
+ // fill-window job so the sweeper settles it through the jobs queue.
+ this.deadlines?.scheduleFillWindow(updated);
+ }
if (this.beginShadowObservation()) {
this.observeAccept(updated ?? intent, solver, updated !== null);
}
+ if (updated === null) return null;
+ // Issue #385: with the onchain rollout on, the accept is not final until
+ // the contract has seen it — broadcast and park pending_accepted.
+ if (await this.isOnchainWrite(updated)) {
+ return this.parkWithBroadcast(updated, "accept", () => [
+ nativeToScVal(updated.intentId, { type: "string" }),
+ new Address(updated.solver ?? solver).toScVal(),
+ nativeToScVal(updated.deadline, { type: "u64" }),
+ ]);
+ }
return updated;
}
@@ -610,7 +878,7 @@ export class IntentsService {
now?: number,
): Promise {
const nowSec = now ?? Math.floor(Date.now() / 1000);
- const updated = await this.repo.fillIfAccepted(id, solver, patch, nowSec);
+ const updated = written(await this.repo.fillIfAccepted(id, solver, patch, nowSec));
if (updated !== null) this.countTransition("accepted", "filled");
if (this.beginShadowObservation()) {
// Report from `patch` rather than re-reading: on a lost race the stored
@@ -620,6 +888,16 @@ export class IntentsService {
// pre-empted it.
this.observeFill(id, solver, patch.fillAmount, patch.txHash, updated !== null);
}
+ if (updated === null) return null;
+ // Issue #385: park the fill pending until the contract confirms it.
+ if (await this.isOnchainWrite(updated)) {
+ return this.parkWithBroadcast(updated, "fill", () => [
+ nativeToScVal(updated.intentId, { type: "string" }),
+ new Address(updated.solver ?? solver).toScVal(),
+ nativeToScVal(BigInt(patch.fillAmount ?? "0"), { type: "i128" }),
+ nativeToScVal(patch.txHash ?? "", { type: "string" }),
+ ]);
+ }
return updated;
}
@@ -651,7 +929,7 @@ export class IntentsService {
* (e.g. a concurrent accept() or sweeper expiry already transitioned it).
*/
async cancelIfOpen(id: string): Promise {
- const updated = await this.repo.cancelIfOpen(id);
+ const updated = written(await this.repo.cancelIfOpen(id));
if (updated !== null) this.countTransition("open", "cancelled");
if (this.beginShadowObservation()) {
const subject = updated ?? (await this.repo.findById(id));
@@ -668,16 +946,118 @@ export class IntentsService {
);
}
}
+ if (updated === null) return null;
+ // Issue #385: the cancellation is not final until the contract has seen
+ // it — broadcast and park pending_cancelled.
+ if (await this.isOnchainWrite(updated)) {
+ return this.parkWithBroadcast(updated, "cancel", () => [
+ nativeToScVal(updated.intentId, { type: "string" }),
+ new Address(updated.user).toScVal(),
+ ]);
+ }
return updated;
}
+ /**
+ * Park an intent in the `pending_*` state matching `op` (issue #385).
+ *
+ * Parking is a confirmation marker, not a lifecycle edge: it records that
+ * the guarded off-chain write for `op` has committed — and, when given, the
+ * hash of its chain broadcast — while the settlement contract has not yet
+ * confirmed it. It therefore writes the state directly rather than through
+ * {@link canTransition}, because `filled → pending_filled` would otherwise
+ * be an illegal edge out of a terminal state. {@link confirmIntent} settles
+ * the marker back to the base state.
+ *
+ * @param intentId - Intent to park.
+ * @param op - Chain write whose confirmation is being awaited.
+ * @param txHash - Broadcast transaction hash, when one was obtained.
+ * @returns The parked intent, or null when it does not exist.
+ */
+ async transitionToOnChainPending(
+ id: string,
+ op: PendingIntentOp,
+ txHash?: string,
+ ): Promise {
+ const intent = await this.repo.findById(id);
+ if (!intent) return null;
+ const updated = written(
+ await this.repo.update(id, {
+ state: PENDING_STATE_BY_OP[op],
+ pendingOp: op,
+ pendingTxHash: txHash,
+ }),
+ );
+ if (updated !== null) this.countTransition(intent.state, updated.state);
+ return updated;
+ }
+
+ /**
+ * Resolve a `pending_*` marker to its confirmed base state (issue #385),
+ * clearing `pendingTxHash`/`pendingOp`.
+ *
+ * @param intentId - Intent whose in-flight write has been observed on chain.
+ * @returns The confirmed intent, or null when it does not exist or is not
+ * in a pending state — confirmation only means something for a write that
+ * is actually in flight.
+ */
+ async confirmIntent(id: string): Promise {
+ const intent = await this.repo.findById(id);
+ if (!intent) return null;
+ const confirmed = CONFIRMED_STATE_BY_PENDING[intent.state];
+ if (!confirmed) return null;
+ const updated = written(
+ await this.repo.update(id, {
+ state: confirmed,
+ pendingTxHash: undefined,
+ pendingOp: undefined,
+ }),
+ );
+ if (updated !== null) this.countTransition(intent.state, confirmed);
+ return updated;
+ }
+
+ /**
+ * Issue #385: attempt the settlement-contract broadcast for a committed
+ * write, then park the intent in its `pending_*` state.
+ *
+ * The park is unconditional — with the onchain rollout on, the base state
+ * is never claimed final — while the broadcast is best-effort: argument
+ * encoding (a malformed address) or an RPC failure logs and parks without
+ * a hash instead of failing a write that has already committed. Settling
+ * the marker is {@link confirmIntent}'s job.
+ */
+ private async parkWithBroadcast(
+ intent: Intent,
+ op: PendingIntentOp,
+ buildArgs: () => xdr.ScVal[],
+ ): Promise {
+ let txHash: string | undefined;
+ try {
+ const contractId = this.configService.get("stellar.settlementContractId", { infer: true });
+ if (!contractId) throw new Error("SETTLEMENT_CONTRACT_ID is not configured");
+ const result = await this.stellarTxService.invokeContract({
+ contractId,
+ method: ONCHAIN_METHOD_BY_OP[op],
+ args: buildArgs(),
+ });
+ txHash = result.hash;
+ } catch (err) {
+ this.logger.error(
+ `On-chain ${op} broadcast failed for intent ${intent.intentId}: ` +
+ `${(err as Error).message} — parked pending without a tx hash`,
+ );
+ }
+ return (await this.transitionToOnChainPending(intent.intentId, op, txHash)) ?? intent;
+ }
+
/**
* Atomically expire an intent only if it is currently "open".
* Used by the sweeper so a concurrent user cancel() or solver accept()
* always wins the race.
*/
async expireIfOpen(id: string): Promise {
- const updated = await this.repo.expireIfOpen(id);
+ const updated = written(await this.repo.expireIfOpen(id));
if (updated !== null) this.countTransition("open", "expired");
if (this.beginShadowObservation()) {
const subject = updated ?? (await this.repo.findById(id));
@@ -705,7 +1085,7 @@ export class IntentsService {
id: string,
patch: { slashedAt: number; slashReason: string },
): Promise {
- const updated = await this.repo.slashIfAccepted(id, patch);
+ const updated = written(await this.repo.slashIfAccepted(id, patch));
if (updated !== null) this.countTransition("accepted", "slashed");
if (this.beginShadowObservation()) {
const subject = updated ?? (await this.repo.findById(id));
@@ -738,7 +1118,9 @@ export class IntentsService {
* or already has a later deadline.
*/
async extendDeadlineIfAccepted(id: string, newDeadline: number): Promise {
- return this.repo.extendDeadlineIfAccepted(id, newDeadline);
+ const updated = written(await this.repo.extendDeadlineIfAccepted(id, newDeadline));
+ if (updated) this.deadlines?.scheduleFillWindow(updated);
+ return updated;
}
// ---------------------------------------------------------------------------
diff --git a/src/intents/intents.types.ts b/src/intents/intents.types.ts
index 7fdda741..b75e5c16 100644
--- a/src/intents/intents.types.ts
+++ b/src/intents/intents.types.ts
@@ -1,12 +1,6 @@
-/**
- * Core intent types for vortex-backend.
- *
- * These types are the source of truth for all modules. The package-level
- * types in src/types/index.ts mirror a subset of these for SDK consumers.
- */
-
-// ─── Chains ──────────────────────────────────────────────────────────────────
+import type { DutchAuction } from "../auctions/dutch";
+/**
* Single source of truth for every chain the protocol recognises.
* `SupportedChain` is derived from this tuple so all three consumers
* (intents.types.ts, create-intent.dto.ts, tokens.data.ts) stay in sync
@@ -24,8 +18,6 @@ export const SUPPORTED_CHAINS = [
export type SupportedChain = (typeof SUPPORTED_CHAINS)[number];
-// ─── Intent states ────────────────────────────────────────────────────────────
-
/**
* The Stellar chain identifier, named for readability at call sites that would
* otherwise repeat the literal.
@@ -69,12 +61,17 @@ export const INTENT_STATES = [
"cancelled",
"expired",
"slashed",
+ // Submitted-but-unconfirmed variants (issue #385). A solver-side write that
+ // has been broadcast but not yet observed on-chain parks the intent in the
+ // matching pending_* state until the confirmation watcher resolves it.
+ "pending_open",
+ "pending_accepted",
+ "pending_filled",
+ "pending_cancelled",
] as const;
export type IntentState = (typeof INTENT_STATES)[number];
-// ─── Token types ──────────────────────────────────────────────────────────────
-
export interface TokenInfo {
address: string;
symbol: string;
@@ -82,7 +79,6 @@ export interface TokenInfo {
decimals: number;
chain: SupportedChain;
logoURI?: string;
- priceUSD?: number | null;
priceUSD?: number;
}
@@ -90,36 +86,6 @@ export interface StellarToken {
contract: string;
symbol: string;
decimals: number;
- priceUSD?: number | null;
-}
-
-// ─── Source-verification ──────────────────────────────────────────────────────
-
-export type VerificationStatus = "pending" | "verified" | "failed" | "grandfathered";
-
-export interface SrcVerificationResult {
- status: VerificationStatus;
- checkedAt: number;
- blockNumber?: string;
- blockHash?: string;
- detail?: string;
- receivedAmount?: string;
-}
-
-// ─── Intent ──────────────────────────────────────────────────────────────────
-
-/**
- * Canonical in-memory representation of a cross-chain swap intent.
- *
- * Bigint amounts (srcAmount, minDstAmount, fillAmount, quotedDstAmount, feeAmount)
- * are stored as decimal strings throughout — never coerced through `Number` so
- * precision is preserved for large ERC-20 amounts.
- *
- * Issue #410 adds `srcTokenId` / `dstTokenId` / `srcDecimals` / `dstDecimals`
- * which are populated by the create path when the token is in the registry.
- * They are intentionally optional so the expand/contract migration can land
- * without breaking the in-memory or dual-write adapters.
- */
priceUSD?: number;
}
@@ -128,14 +94,13 @@ export interface Intent {
user: string;
srcChain: SupportedChain;
srcToken: TokenInfo;
- srcAmount: string;
- dstToken: StellarToken;
- minDstAmount: string;
- quotedDstAmount?: string;
- acceptedDstAmount?: string;
srcAmount: string; // bigint as string
dstToken: StellarToken;
minDstAmount: string;
+ /** Optional off-chain Dutch auction terms attached at creation (issue #507). */
+ auction?: DutchAuction;
+ /** Destination amount locked when a solver accepted the intent. */
+ acceptedDstAmount?: string;
quotedDstAmount?: string; // best quote from solvers
solver?: string;
state: IntentState;
@@ -143,42 +108,14 @@ export interface Intent {
deadline: number;
filledAt?: number;
fillAmount?: string;
- feeAmount?: string;
- txHash?: string;
- slashedAt?: number;
- slashReason?: string;
-
- // Optimistic concurrency (#404)
- version: number;
-
- // Dutch auction (#429)
- auction?: Record;
-
- // Source-deposit verification (#403)
- srcVerified: boolean;
- srcTxHash?: string;
- srcVerification?: SrcVerificationResult;
-
- // Governance params snapshot at creation
- paramsVersion?: string;
-
- // ── #410: FK columns (populated at create-time when token is in registry) ──
- srcTokenId?: string;
- dstTokenId?: string;
- /** Immutable snapshot of src token decimals at intent creation time. */
- srcDecimals?: number;
- /** Immutable snapshot of dst token decimals at intent creation time. */
- dstDecimals?: number;
-}
-
-export interface IntentAuditEntry {
- timestamp: string;
- toState: IntentState;
- actor: string;
- reason: string;
- metadata?: Record;
feeAmount?: string; // realized protocol fee in dst token base units
txHash?: string; // fill tx on Stellar
+ /** Fill verification state written by the fill verifier (issue #471). */
+ fillVerificationState?: "pending" | "verified" | "rejected";
+ /** Human-readable reason when fill verification rejected the fill. */
+ fillVerificationReason?: string;
+ /** ISO-8601 timestamp of the last fill-verification attempt. */
+ fillVerifiedAt?: string;
slashedAt?: number;
slashReason?: string;
/**
@@ -188,6 +125,85 @@ export interface IntentAuditEntry {
* Absent on intents created before issue #500 was deployed.
*/
paramsVersion?: number;
+ /**
+ * USD value of `srcAmount` at creation time, computed from the resolved
+ * source-token price. Powers the `minAmountUsd` / `maxAmountUsd` filters
+ * and USD sorting (issue #440). `undefined` when the token price was
+ * unknown at creation — historical rows are never backfilled with
+ * fabricated values.
+ */
+ usdValueAtCreate?: number;
+ /**
+ * Optimistic-concurrency version (issue #405). Starts at 0 on creation and
+ * is incremented by exactly one on every successful mutation. Exposed to
+ * HTTP clients as the `ETag` of `GET /api/v1/intents/:id`.
+ *
+ * Optional because records constructed before issue #405 (and any caller
+ * that omits it) predate OCC: repositories treat an absent value as `0`,
+ * and the Prisma adapter normalises it to the column default on write.
+ */
+ version?: number;
+ /**
+ * Whether the user's source-chain deposit has been verified (issue #403).
+ * Intents stay `open` but are hidden from `GET /intents/open` and cannot be
+ * accepted until this is true.
+ *
+ * Optional with an implicit `false` for records created before issue #403 —
+ * the Prisma adapter writes the column default when the field is absent.
+ */
+ srcVerified?: boolean;
+ /** Source-chain transaction that performed the escrow deposit, if supplied. */
+ srcTxHash?: string;
+ /** Details of the most recent source-deposit verification attempt. */
+ srcVerification?: SrcVerification;
+ /**
+ * Hash of the on-chain transaction a solver-side write is waiting on
+ * (issue #385). Set together with `pendingOp` when the intent enters a
+ * pending_* state; cleared once the confirmation watcher resolves it.
+ */
+ pendingTxHash?: string;
+ /** The operation whose on-chain confirmation is being awaited. */
+ pendingOp?: PendingIntentOp;
+ /**
+ * Registry token identifiers and decimals (issue #410). Populated by the
+ * create path when the token is in the registry; intentionally optional so
+ * the expand/contract migration can land without breaking the in-memory or
+ * dual-write adapters.
+ */
+ srcTokenId?: string;
+ dstTokenId?: string;
+ srcDecimals?: number;
+ dstDecimals?: number;
+}
+
+/**
+ * Which state transition is in flight while an intent sits in a pending_*
+ * state (issue #385).
+ */
+export type PendingIntentOp = "create" | "accept" | "fill" | "cancel";
+
+/** Outcome of the source-chain deposit check for an intent (issue #403). */
+export type SrcVerificationStatus =
+ | "pending"
+ | "verified"
+ | "not_found"
+ | "mismatch"
+ | "reorged"
+ | "skipped"
+ | "grandfathered";
+
+export interface SrcVerification {
+ status: SrcVerificationStatus;
+ /** Unix epoch seconds of the last check. */
+ checkedAt: number;
+ /** Block the matching `Deposited` log was found in. */
+ blockNumber?: string;
+ /** Hash of that block — compared on re-checks to detect reorgs. */
+ blockHash?: string;
+ /** Amount the escrow actually received (base units) — may be < srcAmount for fee-on-transfer tokens. */
+ receivedAmount?: string;
+ /** Human-readable reason for a non-verified status. */
+ detail?: string;
}
export interface Quote {
diff --git a/src/intents/prisma-intents.repository.ts b/src/intents/prisma-intents.repository.ts
index 5bb65ec3..a6989376 100644
--- a/src/intents/prisma-intents.repository.ts
+++ b/src/intents/prisma-intents.repository.ts
@@ -1,290 +1,294 @@
-import { Injectable, Logger } from "@nestjs/common";
+import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import {
- IIntentsRepository,
IdempotentCreateResult,
+ IIntentsRepository,
IntentPatch,
+ IntentSearchQuery,
+ IntentSearchResult,
MutationResult,
VersionConflict,
} from "./intents.repository";
-import { Intent, IntentState, TokenInfo, StellarToken } from "./intents.types";
-
-// ─── Row → domain mappers ─────────────────────────────────────────────────────
-
-type IntentRow = {
- id: string;
- intentId: string;
- user: string;
- srcChain: string;
- srcToken: Prisma.JsonValue;
- srcAmount: string;
- dstToken: Prisma.JsonValue;
- minDstAmount: string;
- quotedDstAmount?: string | null;
- acceptedDstAmount?: string | null;
- solver?: string | null;
- state: string;
- createdAt: number;
- deadline: number;
- filledAt?: number | null;
- fillAmount?: string | null;
- feeAmount?: string | null;
- txHash?: string | null;
- slashedAt?: number | null;
- slashReason?: string | null;
- version: number;
- idempotencyKey?: string | null;
- auction?: Prisma.JsonValue | null;
- srcVerified: boolean;
- srcTxHash?: string | null;
- srcVerification?: Prisma.JsonValue | null;
- paramsVersion?: string | null;
- // #410 FK columns
- srcTokenId?: string | null;
- dstTokenId?: string | null;
- srcDecimals?: number | null;
- dstDecimals?: number | null;
-};
-
-function rowToIntent(row: IntentRow): Intent {
- return {
- intentId: row.intentId,
- user: row.user,
- srcChain: row.srcChain as Intent["srcChain"],
- srcToken: row.srcToken as unknown as TokenInfo,
- srcAmount: row.srcAmount,
- dstToken: row.dstToken as unknown as StellarToken,
- minDstAmount: row.minDstAmount,
- quotedDstAmount: row.quotedDstAmount ?? undefined,
- acceptedDstAmount: row.acceptedDstAmount ?? undefined,
- solver: row.solver ?? undefined,
- state: row.state as IntentState,
- createdAt: row.createdAt,
- deadline: row.deadline,
- filledAt: row.filledAt ?? undefined,
- fillAmount: row.fillAmount ?? undefined,
- feeAmount: row.feeAmount ?? undefined,
- txHash: row.txHash ?? undefined,
- slashedAt: row.slashedAt ?? undefined,
- slashReason: row.slashReason ?? undefined,
- version: row.version,
- auction: row.auction as Record | undefined,
- srcVerified: row.srcVerified,
- srcTxHash: row.srcTxHash ?? undefined,
- srcVerification: row.srcVerification as Intent["srcVerification"],
- paramsVersion: row.paramsVersion ?? undefined,
- srcTokenId: row.srcTokenId ?? undefined,
- dstTokenId: row.dstTokenId ?? undefined,
- srcDecimals: row.srcDecimals ?? undefined,
- dstDecimals: row.dstDecimals ?? undefined,
- };
-}
+import { Intent, IntentState, StellarToken, TokenInfo } from "./intents.types";
+import { IntentState as PrismaIntentState, Prisma } from "@prisma/client";
-function intentToCreateData(intent: Intent): Record {
- return {
- id: intent.intentId, // use intentId as Prisma id for upsert simplicity
- intentId: intent.intentId,
- user: intent.user,
- srcChain: intent.srcChain as string,
- srcToken: intent.srcToken as unknown,
- srcAmount: intent.srcAmount,
- dstToken: intent.dstToken as unknown,
- minDstAmount: intent.minDstAmount,
- quotedDstAmount: intent.quotedDstAmount ?? null,
- acceptedDstAmount: intent.acceptedDstAmount ?? null,
- solver: intent.solver ?? null,
- state: intent.state as string,
- createdAt: intent.createdAt,
- deadline: intent.deadline,
- filledAt: intent.filledAt ?? null,
- fillAmount: intent.fillAmount ?? null,
- feeAmount: intent.feeAmount ?? null,
- txHash: intent.txHash ?? null,
- slashedAt: intent.slashedAt ?? null,
- slashReason: intent.slashReason ?? null,
- version: intent.version,
- idempotencyKey: intent.intentId, // use intentId as idempotency default
- auction: intent.auction ?? null,
- srcVerified: intent.srcVerified,
- srcTxHash: intent.srcTxHash ?? null,
- srcVerification: intent.srcVerification ?? null,
- paramsVersion: intent.paramsVersion ?? null,
- srcTokenId: intent.srcTokenId ?? null,
- dstTokenId: intent.dstTokenId ?? null,
- srcDecimals: intent.srcDecimals ?? null,
- dstDecimals: intent.dstDecimals ?? null,
- };
-}
-
-// ─── Repository ───────────────────────────────────────────────────────────────
+/** PrismaService, or the client handed to a `$transaction` callback (issue #396). */
+export type IntentsPrismaClient = PrismaService | Prisma.TransactionClient;
/**
- * Prisma-backed implementation of IIntentsRepository (#404 / #405 / #410).
+ * Prisma-backed implementation of IIntentsRepository.
*
- * Optimistic concurrency is enforced via `WHERE version = $expected` predicates
- * in raw UPDATE statements for all guarded transitions so they remain atomic
- * under concurrent load.
+ * All mutating operations that must be race-free (`acceptIfOpen`,
+ * `fillIfAccepted`) use a single conditional `updateMany` call so the
+ * database enforces the state guard atomically — no separate read-then-write.
*
- * Issue #410: `save()` attempts to populate srcTokenId / dstTokenId / snapshot
- * decimals by joining against the tokens table using (address, chain). If the
- * token is not in the registry the FK columns are left null — the JSON blob
- * path remains valid.
+ * Bigint amounts (srcAmount, minDstAmount, fillAmount, quotedDstAmount) are
+ * stored and returned as strings per the project's bigint-as-string convention
+ * (see CONTRIBUTING.md). JSON columns (srcToken, dstToken) are cast back to
+ * their TypeScript types on the way out.
*/
@Injectable()
export class PrismaIntentsRepository implements IIntentsRepository {
- private readonly logger = new Logger(PrismaIntentsRepository.name);
+ constructor(private readonly prisma: IntentsPrismaClient) {}
+
+ async save(intent: Intent): Promise {
+ const data = this.toDbData(intent);
+ await this.prisma.intent.upsert({
+ where: { intentId: intent.intentId },
+ create: { ...data, intentId: intent.intentId },
+ update: data,
+ });
+ return intent;
+ }
+
+ /**
+ * Mirror a row only when the incoming record is strictly newer than what is
+ * stored (issue #405): a late-arriving out-of-order mirror write must never
+ * regress a row to an older `version`. A missing row is created; an equal or
+ * older version is dropped silently.
+ */
+ async saveIfNewer(intent: Intent): Promise {
+ const data = this.toDbData(intent);
+ const updated = await this.prisma.intent.updateMany({
+ where: { intentId: intent.intentId, version: { lt: intent.version ?? 0 } },
+ data,
+ });
+ if (updated.count > 0) return;
+ const existing = await this.prisma.intent.findUnique({ where: { intentId: intent.intentId } });
+ if (existing) return; // stored version is equal or newer — keep it.
+ try {
+ await this.prisma.intent.create({ data: { ...data, intentId: intent.intentId } });
+ } catch (err) {
+ // P2002 = a concurrent writer created the row first; their version wins.
+ if ((err as Prisma.PrismaClientKnownRequestError).code !== "P2002") throw err;
+ }
+ }
- constructor(private readonly prisma: PrismaService) {}
+ /**
+ * Insert `intent` unless an intent created at or after `minCreatedAt`
+ * already holds `idempotencyKey` (issue #404). Atomic across replicas via
+ * the unique index on `idempotency_key`; a key held only by an intent older
+ * than the replay window is released and reused.
+ */
+ async createIdempotent(
+ intent: Intent,
+ idempotencyKey: string,
+ minCreatedAt: number,
+ ): Promise {
+ const replay = await this.findByIdempotencyKey(idempotencyKey, minCreatedAt);
+ if (replay) return { intent: replay, created: false };
- // ── Read methods ──────────────────────────────────────────────────────────
+ await this.prisma.intent.updateMany({
+ where: { idempotencyKey, createdAt: { lt: minCreatedAt } },
+ data: { idempotencyKey: null },
+ });
+
+ try {
+ await this.prisma.intent.create({
+ data: { ...this.toDbData(intent), intentId: intent.intentId, idempotencyKey },
+ });
+ return { intent, created: true };
+ } catch (err) {
+ if ((err as Prisma.PrismaClientKnownRequestError).code === "P2002") {
+ const winner = await this.findByIdempotencyKey(idempotencyKey, minCreatedAt);
+ if (winner) return { intent: winner, created: false };
+ }
+ throw err;
+ }
+ }
+
+ /** Find the unexpired intent created with `idempotencyKey`, if any. */
+ async findByIdempotencyKey(idempotencyKey: string, minCreatedAt: number): Promise {
+ const row = await this.prisma.intent.findFirst({
+ where: { idempotencyKey, createdAt: { gte: minCreatedAt } },
+ });
+ return row ? this.fromRow(row) : undefined;
+ }
+
+ /** Fetch several intents in one call; unknown IDs are omitted. */
+ async findManyByIds(ids: string[]): Promise {
+ const unique = [...new Set(ids)];
+ if (unique.length === 0) return [];
+ const rows = await this.prisma.intent.findMany({ where: { intentId: { in: unique } } });
+ return rows.map((r) => this.fromRow(r));
+ }
async findById(id: string): Promise {
const row = await this.prisma.intent.findUnique({ where: { intentId: id } });
- return row ? rowToIntent(row as IntentRow) : undefined;
+ return row ? this.fromRow(row) : undefined;
}
async findAll(): Promise {
const rows = await this.prisma.intent.findMany({
- orderBy: [{ createdAt: "desc" }, { intentId: "asc" }],
+ orderBy: { createdAt: "desc" },
});
- return rows.map((r) => rowToIntent(r as IntentRow));
+ return rows.map((r) => this.fromRow(r));
}
async findByState(state: IntentState): Promise {
const rows = await this.prisma.intent.findMany({
- where: { state: state as string },
- orderBy: [{ createdAt: "desc" }, { intentId: "asc" }],
+ where: { state: this.toPrismaState(state) },
+ orderBy: { createdAt: "desc" },
});
- return rows.map((r) => rowToIntent(r as IntentRow));
+ return rows.map((r) => this.fromRow(r));
}
async findByUser(user: string): Promise {
+ // Postgres is case-sensitive; normalise the address comparison in-query.
const rows = await this.prisma.intent.findMany({
where: { user: { equals: user, mode: "insensitive" } },
- orderBy: [{ createdAt: "desc" }, { intentId: "asc" }],
- });
- return rows.map((r) => rowToIntent(r as IntentRow));
- }
-
- async findManyByIds(ids: string[]): Promise {
- if (ids.length === 0) return [];
- const unique = [...new Set(ids)];
- const rows = await this.prisma.intent.findMany({
- where: { intentId: { in: unique } },
+ orderBy: { createdAt: "desc" },
});
- return rows.map((r) => rowToIntent(r as IntentRow));
+ return rows.map((r) => this.fromRow(r));
}
+ /** Number of intents currently `accepted` by `solver` (issue #405). */
async countAcceptedBySolver(solver: string): Promise {
return this.prisma.intent.count({
- where: { solver: { equals: solver, mode: "insensitive" }, state: "accepted" as string },
+ where: { state: PrismaIntentState.accepted, solver },
});
}
+ /** Number of `open` or `accepted` intents owned by `user` (case-insensitive). */
async countActiveByUser(user: string): Promise {
return this.prisma.intent.count({
where: {
user: { equals: user, mode: "insensitive" },
- state: { in: ["open", "accepted"] as string[] },
+ state: { in: [PrismaIntentState.open, PrismaIntentState.accepted] },
},
});
}
- findByIdempotencyKey(key: string, minCreatedAt: number): Promise {
- return this.prisma.intent
- .findFirst({
- where: { idempotencyKey: key, createdAt: { gte: minCreatedAt } },
- })
- .then((row) => (row ? rowToIntent(row as IntentRow) : undefined));
- }
+ /**
+ * Advanced search (issue #440) — filtering, sorting and pagination pushed
+ * into the database query.
+ *
+ * Scalar filters use Prisma's typed where input; the JSONB token-symbol
+ * filters use raw SQL (`lower(token->>'symbol') = lower($1)`) so they can
+ * use the expression indexes created in the migration and stay
+ * case-insensitive. Sorting and pagination are applied in the same query so
+ * no supported combination falls back to a sequential scan + app-side slice.
+ */
+ async search(query: IntentSearchQuery): Promise {
+ const where: Prisma.IntentWhereInput = {};
+
+ if (query.state !== undefined) where.state = this.toPrismaState(query.state);
+ if (query.chain !== undefined) where.srcChain = query.chain as Prisma.IntentWhereInput["srcChain"];
+ if (query.user !== undefined) where.user = { equals: query.user, mode: "insensitive" };
+ if (query.solver !== undefined) where.solver = { equals: query.solver, mode: "insensitive" };
+
+ if (query.minAmountUsd !== undefined && query.maxAmountUsd !== undefined) {
+ where.usdValueAtCreate = { gte: query.minAmountUsd, lte: query.maxAmountUsd };
+ } else if (query.minAmountUsd !== undefined) {
+ where.usdValueAtCreate = { gte: query.minAmountUsd };
+ } else if (query.maxAmountUsd !== undefined) {
+ where.usdValueAtCreate = { lte: query.maxAmountUsd };
+ }
- // ── Write methods ─────────────────────────────────────────────────────────
+ if (query.createdFrom !== undefined && query.createdTo !== undefined) {
+ where.createdAt = { gte: query.createdFrom, lte: query.createdTo };
+ } else if (query.createdFrom !== undefined) {
+ where.createdAt = { gte: query.createdFrom };
+ } else if (query.createdTo !== undefined) {
+ where.createdAt = { lte: query.createdTo };
+ }
+
+ // JSONB token-symbol filters — raw SQL so the expression indexes apply.
+ const raw: Prisma.Sql[] = [];
+ if (query.srcToken !== undefined) {
+ raw.push(Prisma.sql`lower(src_token->>'symbol') = lower(${query.srcToken})`);
+ }
+ if (query.dstToken !== undefined) {
+ raw.push(Prisma.sql`lower(dst_token->>'symbol') = lower(${query.dstToken})`);
+ }
+ if (raw.length > 0) {
+ (where as { AND: unknown }).AND = raw;
+ }
+
+ // Sorting — default createdAt desc (preserves pre-existing behaviour).
+ const [dimension, direction] = (query.sort ?? "created:desc").split(":");
+ const dir = direction === "asc" ? ("asc" as const) : ("desc" as const);
+ let orderBy: Prisma.IntentOrderByWithRelationInput;
+ switch (dimension) {
+ case "deadline":
+ orderBy = { deadline: dir };
+ break;
+ case "usd":
+ orderBy = { usdValueAtCreate: dir };
+ break;
+ case "created":
+ default:
+ orderBy = { createdAt: dir };
+ }
+
+ const offset = query.offset ?? 0;
+ const limit = query.limit ?? 20;
+
+ const [rows, total] = await Promise.all([
+ this.prisma.intent.findMany({ where, orderBy, skip: offset, take: limit }),
+ this.prisma.intent.count({ where }),
+ ]);
+
+ return { intents: rows.map((r) => this.fromRow(r)), total };
+ }
/**
- * Save (upsert) an intent. Attempts to resolve token FK columns (#410)
- * when `srcTokenId` / `dstTokenId` are not already set.
+ * Apply `patch` only if the stored version equals `expectedVersion`
+ * (issue #405): zero rows updated means the intent is absent (`null`) or
+ * another writer already bumped the version ({@link VersionConflict} with
+ * the version actually found, so the caller can re-read and retry).
*/
- async save(intent: Intent): Promise {
- const withFk = await this.resolveTokenFks(intent);
- const data = intentToCreateData(withFk);
- const row = await this.prisma.intent.upsert({
- where: { intentId: intent.intentId },
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- create: data as any,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- update: (({ id: _id, ...rest }) => rest)(data) as any,
+ async update(id: string, patch: IntentPatch, expectedVersion?: number): Promise {
+ const result = await this.prisma.intent.updateMany({
+ where: { intentId: id, ...(expectedVersion !== undefined ? { version: expectedVersion } : {}) },
+ data: { ...this.toDbPatch(patch), version: { increment: 1 } },
});
- return rowToIntent(row as IntentRow);
+ if (result.count === 0) return this.resolveMiss(id, expectedVersion);
+ const row = await this.prisma.intent.findUnique({ where: { intentId: id } });
+ return row ? this.fromRow(row) : null;
}
/**
- * Version-guarded upsert for the dual-write mirror.
- * Saves only when incoming version >= stored version.
+ * Amend the user-adjustable fields of an `open` intent whose existing
+ * deadline is still in the future (issue #569). The state + deadline
+ * predicates make the write race-free; `version` is intentionally not
+ * bumped (an amend widens terms, it does not compete with solver writes).
*/
- async saveIfNewer(intent: Intent): Promise {
- const existing = await this.findById(intent.intentId);
- if (existing && existing.version > intent.version) return existing;
- return this.save(intent);
- }
-
- async createIdempotent(
- intent: Intent,
- idempotencyKey: string,
- minCreatedAt: number,
- ): Promise {
- // Check for existing key first (read-before-write is safe here because the
- // UNIQUE constraint on idempotency_key makes the INSERT below atomic).
- const existing = await this.findByIdempotencyKey(idempotencyKey, minCreatedAt);
- if (existing) return { intent: existing, created: false };
-
- const withFk = await this.resolveTokenFks(intent);
- const data = intentToCreateData(withFk);
- // Override the idempotency key with the caller-supplied one.
- data.idempotencyKey = idempotencyKey;
-
- try {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const row = await this.prisma.intent.create({ data: data as any });
- return { intent: rowToIntent(row as IntentRow), created: true };
- } catch (err) {
- // Unique constraint violation → another replica created first.
- if ((err as { code?: string }).code === "P2002") {
- const replay = await this.findByIdempotencyKey(idempotencyKey, minCreatedAt);
- if (replay) return { intent: replay, created: false };
- }
- throw err;
- }
- }
-
- async update(id: string, patch: IntentPatch, expectedVersion: number): Promise {
- return this.prisma.withDefaultTimeout(async (tx) => {
- const existing = await (tx as unknown as typeof this.prisma).intent.findUnique({
- where: { intentId: id },
- });
- if (!existing) return null;
- const current = existing as IntentRow;
- if (current.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, current.version);
- }
- const updated = await (tx as unknown as typeof this.prisma).intent.update({
- where: { intentId: id, version: expectedVersion },
- data: { ...(patch as Record), version: expectedVersion + 1 },
- });
- return updated ? rowToIntent(updated as IntentRow) : null;
+ async amendIfOpen(
+ id: string,
+ patch: Pick,
+ now = Math.floor(Date.now() / 1000),
+ ): Promise {
+ const result = await this.prisma.intent.updateMany({
+ where: { intentId: id, state: PrismaIntentState.open, deadline: { gt: now } },
+ data: { minDstAmount: patch.minDstAmount, deadline: patch.deadline },
});
+ if (result.count === 0) return null;
+ const row = await this.prisma.intent.findUnique({ where: { intentId: id } });
+ return row ? this.fromRow(row) : null;
}
async delete(id: string): Promise {
try {
await this.prisma.intent.delete({ where: { intentId: id } });
return true;
- } catch {
- return false;
+ } catch (err) {
+ if ((err as Prisma.PrismaClientKnownRequestError).code === "P2025") return false;
+ throw err;
}
}
- // ── Atomic transitions ────────────────────────────────────────────────────
-
+ /**
+ * Atomically accept an intent only when it is currently `open` AND its
+ * deadline is still in the future (issue #473).
+ *
+ * Uses a single `updateMany` with a compound WHERE clause so the database
+ * enforces the state + deadline guards — zero rows updated means another
+ * solver already won the race or the sweeper already expired the intent.
+ *
+ * Lock ordering: callers enforcing per-solver caps must hold the solver
+ * advisory lock (`pg_advisory_xact_lock`) BEFORE calling this method.
+ */
async acceptIfOpen(
id: string,
solver: string,
@@ -293,242 +297,386 @@ export class PrismaIntentsRepository implements IIntentsRepository {
expectedVersion?: number,
): Promise {
const nowSec = now ?? Math.floor(Date.now() / 1000);
- return this.prisma.withDefaultTimeout(async (tx) => {
- const existing = await (tx as unknown as typeof this.prisma).intent.findUnique({
- where: { intentId: id },
- });
- if (!existing) return null;
- const row = existing as IntentRow;
- if (row.state !== "open" || row.deadline <= nowSec) return null;
- if (expectedVersion !== undefined && row.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, row.version);
- }
- const updated = await (tx as unknown as typeof this.prisma).intent.updateMany({
- where: {
- intentId: id,
- state: "open",
- deadline: { gt: nowSec },
- version: row.version,
- },
- data: {
- state: "accepted",
- solver,
- deadline: newDeadline,
- version: row.version + 1,
- },
- });
- if (updated.count === 0) return null;
- return rowToIntent({ ...row, state: "accepted", solver, deadline: newDeadline, version: row.version + 1 });
+ const result = await this.prisma.intent.updateMany({
+ where: {
+ intentId: id,
+ state: PrismaIntentState.open,
+ deadline: { gt: nowSec },
+ ...(expectedVersion !== undefined ? { version: expectedVersion } : {}),
+ },
+ data: {
+ state: PrismaIntentState.accepted,
+ solver,
+ deadline: newDeadline,
+ version: { increment: 1 },
+ },
});
- }
- async acceptIfOpenWithinExposure(
- id: string,
- solver: string,
- newDeadline: number,
- now: number,
- candidateExposureUsdMicros: bigint,
- maxExposureUsdMicros: bigint,
- ): Promise<{ intent: Intent | null; exposureExceeded: boolean }> {
- // Serialise per-solver using a Postgres advisory lock so only one
- // concurrent request can evaluate the exposure cap for a given solver.
- const lockKey = this.solverAdvisoryLockKey(solver);
- return this.prisma.withDefaultTimeout(async (tx) => {
- const rawTx = tx as unknown as { $executeRaw: typeof this.prisma.$executeRaw };
- await rawTx.$executeRaw`SELECT pg_advisory_xact_lock(${lockKey})`;
-
- const existing = await (tx as unknown as typeof this.prisma).intent.findUnique({
- where: { intentId: id },
- });
- if (!existing) return { intent: null, exposureExceeded: false };
- const row = existing as IntentRow;
- if (row.state !== "open" || row.deadline <= now) {
- return { intent: null, exposureExceeded: false };
- }
+ if (result.count === 0) return this.resolveMiss(id, expectedVersion);
- // Sum exposure for accepted intents belonging to this solver.
- // srcAmount is TEXT — cast to NUMERIC for SUM via raw query.
- const rawResult = await (tx as unknown as typeof this.prisma).$queryRaw>`
- SELECT COALESCE(SUM(src_amount::numeric)::text, '0') AS total
- FROM intents
- WHERE LOWER(solver) = LOWER(${solver})
- AND state = 'accepted'
- AND deadline > ${now}
- `;
- const acceptedExposure = BigInt(rawResult[0]?.total ?? "0");
-
- if (acceptedExposure + candidateExposureUsdMicros > maxExposureUsdMicros) {
- return { intent: null, exposureExceeded: true };
- }
+ // Fetch the updated row to return the full intent shape.
+ const row = await this.prisma.intent.findUnique({ where: { intentId: id } });
+ return row ? this.fromRow(row) : null;
+ }
- const updated = await (tx as unknown as typeof this.prisma).intent.updateMany({
- where: { intentId: id, state: "open", version: row.version },
- data: { state: "accepted", solver, deadline: newDeadline, version: row.version + 1 },
- });
- if (updated.count === 0) return { intent: null, exposureExceeded: false };
- return {
- intent: rowToIntent({ ...row, state: "accepted", solver, deadline: newDeadline, version: row.version + 1 }),
- exposureExceeded: false,
- };
+ /**
+ * Acquire a transaction-scoped advisory lock for a solver key (issue #473).
+ *
+ * Must be called inside a `$transaction` callback to serialize per-solver
+ * cap checks across replicas. Lock ordering: solver lock BEFORE any intent
+ * row write, released automatically at transaction end. No-op fallback when
+ * the Prisma client does not expose `$executeRaw` (e.g. unit tests).
+ */
+ async acquireSolverLock(solver: string): Promise {
+ const client = this.prisma as unknown as {
+ $executeRaw?: (q: TemplateStringsArray, ...v: unknown[]) => Promise;
+ };
+ if (typeof client.$executeRaw !== "function") return;
+ await client.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${solver}))`;
+ }
+
+ /** Count open/accepted intents for a user with a single COUNT query. */
+ async countOpenByUser(user: string): Promise {
+ return this.prisma.intent.count({
+ where: {
+ user: { equals: user, mode: "insensitive" },
+ state: { in: [PrismaIntentState.open, PrismaIntentState.accepted] },
+ },
});
}
+ /**
+ * Atomically fill an intent only when it is currently `accepted` by the
+ * specified solver AND the fill window has not elapsed (issue #473).
+ *
+ * Uses a single `updateMany` with a compound WHERE clause — zero rows
+ * updated means the intent was not in the expected state, is assigned to a
+ * different solver, or the deadline passed (sweeper wins).
+ */
async fillIfAccepted(
id: string,
solver: string,
- patch: Pick, "filledAt" | "fillAmount" | "feeAmount" | "txHash">,
+ patch: Omit, "state" | "solver">,
now?: number,
expectedVersion?: number,
): Promise {
const nowSec = now ?? Math.floor(Date.now() / 1000);
- return this.prisma.withDefaultTimeout(async (tx) => {
- const existing = await (tx as unknown as typeof this.prisma).intent.findUnique({
- where: { intentId: id },
- });
- if (!existing) return null;
- const row = existing as IntentRow;
- if (row.state !== "accepted" || row.solver !== solver || row.deadline <= nowSec) return null;
- if (expectedVersion !== undefined && row.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, row.version);
- }
- const updated = await (tx as unknown as typeof this.prisma).intent.updateMany({
- where: { intentId: id, state: "accepted", solver, version: row.version, deadline: { gt: nowSec } },
- data: { ...patch, state: "filled", version: row.version + 1 },
- });
- if (updated.count === 0) return null;
- return rowToIntent({ ...row, ...patch, state: "filled", version: row.version + 1 });
+ const result = await this.prisma.intent.updateMany({
+ where: {
+ intentId: id,
+ state: PrismaIntentState.accepted,
+ solver,
+ deadline: { gt: nowSec },
+ ...(expectedVersion !== undefined ? { version: expectedVersion } : {}),
+ },
+ data: {
+ state: PrismaIntentState.filled,
+ version: { increment: 1 },
+ ...(patch.filledAt !== undefined ? { filledAt: patch.filledAt } : {}),
+ ...(patch.fillAmount !== undefined ? { fillAmount: patch.fillAmount } : {}),
+ ...(patch.feeAmount !== undefined
+ ? { feeAmount: patch.feeAmount as string }
+ : {}),
+ ...(patch.txHash !== undefined ? { txHash: patch.txHash } : {}),
+ },
});
+
+ if (result.count === 0) return this.resolveMiss(id, expectedVersion);
+
+ const row = await this.prisma.intent.findUnique({ where: { intentId: id } });
+ return row ? this.fromRow(row) : null;
}
- async cancelIfOpen(id: string, expectedVersion?: number): Promise {
- return this.conditionalTransition(id, "open", "cancelled", {}, expectedVersion);
+ /**
+ * Atomically claims a tx hash for one accepted intent. The unique index on
+ * intents.tx_hash arbitrates cross-intent races; the row predicate arbitrates
+ * concurrent attempts to replace a hash on the same intent.
+ */
+ async reserveFillTxHash(id: string, solver: string, txHash: string): Promise {
+ const result = await this.prisma.intent.updateMany({
+ where: {
+ intentId: id,
+ state: PrismaIntentState.accepted,
+ solver,
+ OR: [{ txHash: null }, { txHash }],
+ },
+ data: {
+ txHash,
+ fillVerificationState: "pending",
+ fillVerificationReason: null,
+ },
+ });
+ if (result.count === 0) return null;
+ const row = await this.prisma.intent.findUnique({ where: { intentId: id } });
+ return row ? this.fromRow(row) : null;
}
- async expireIfOpen(id: string, expectedVersion?: number): Promise {
- return this.conditionalTransition(id, "open", "expired", {}, expectedVersion);
+ /**
+ * Atomically cancel an intent only when it is currently `open`. Guards
+ * against a concurrent solver accept() or sweeper expiry on the same intent.
+ */
+ async cancelIfOpen(id: string, expectedVersion?: number): Promise {
+ const result = await this.prisma.intent.updateMany({
+ where: {
+ intentId: id,
+ state: PrismaIntentState.open,
+ ...(expectedVersion !== undefined ? { version: expectedVersion } : {}),
+ },
+ data: { state: PrismaIntentState.cancelled, version: { increment: 1 } },
+ });
+
+ if (result.count === 0) return this.resolveMiss(id, expectedVersion);
+
+ const row = await this.prisma.intent.findUnique({ where: { intentId: id } });
+ return row ? this.fromRow(row) : null;
}
- async extendDeadlineIfAccepted(
- id: string,
- newDeadline: number,
- expectedVersion?: number,
- ): Promise {
- return this.prisma.withDefaultTimeout(async (tx) => {
- const existing = await (tx as unknown as typeof this.prisma).intent.findUnique({
- where: { intentId: id },
- });
- if (!existing) return null;
- const row = existing as IntentRow;
- if (row.state !== "accepted" || row.deadline >= newDeadline) return null;
- if (expectedVersion !== undefined && row.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, row.version);
- }
- const updated = await (tx as unknown as typeof this.prisma).intent.updateMany({
- where: { intentId: id, state: "accepted", version: row.version, deadline: { lt: newDeadline } },
- data: { deadline: newDeadline, version: row.version + 1 },
- });
- if (updated.count === 0) return null;
- return rowToIntent({ ...row, deadline: newDeadline, version: row.version + 1 });
+ /**
+ * Atomically expire an intent only when it is currently `open`. Used by the
+ * sweeper so a concurrent user cancel() or solver accept() always wins the race.
+ */
+ async expireIfOpen(id: string, expectedVersion?: number): Promise {
+ const result = await this.prisma.intent.updateMany({
+ where: {
+ intentId: id,
+ state: PrismaIntentState.open,
+ ...(expectedVersion !== undefined ? { version: expectedVersion } : {}),
+ },
+ data: { state: PrismaIntentState.expired, version: { increment: 1 } },
});
+
+ if (result.count === 0) return this.resolveMiss(id, expectedVersion);
+
+ const row = await this.prisma.intent.findUnique({ where: { intentId: id } });
+ return row ? this.fromRow(row) : null;
}
+ /**
+ * Atomically slash an intent only when it is currently `accepted`. Used by
+ * the sweeper so a concurrent solver fill() always wins the race.
+ */
async slashIfAccepted(
id: string,
patch: { slashedAt: number; slashReason: string },
expectedVersion?: number,
): Promise {
- return this.conditionalTransition(id, "accepted", "slashed", patch, expectedVersion);
- }
+ const result = await this.prisma.intent.updateMany({
+ where: {
+ intentId: id,
+ state: PrismaIntentState.accepted,
+ ...(expectedVersion !== undefined ? { version: expectedVersion } : {}),
+ },
+ data: {
+ state: PrismaIntentState.slashed,
+ version: { increment: 1 },
+ slashedAt: patch.slashedAt,
+ slashReason: patch.slashReason,
+ },
+ });
+
+ if (result.count === 0) return this.resolveMiss(id, expectedVersion);
- // ── Private helpers ───────────────────────────────────────────────────────
+ const row = await this.prisma.intent.findUnique({ where: { intentId: id } });
+ return row ? this.fromRow(row) : null;
+ }
/**
- * Generic conditional-transition helper for simple state guards.
+ * Issue #477 — push an accepted intent's deadline out during a fill pause.
+ * The `deadline < newDeadline` predicate makes this a no-op once the window
+ * is already long enough, so repeated sweep cycles cannot creep the deadline
+ * forward indefinitely.
*/
- private async conditionalTransition(
+ async extendDeadlineIfAccepted(
id: string,
- fromState: IntentState,
- toState: IntentState,
- extra: Record,
+ newDeadline: number,
expectedVersion?: number,
): Promise {
- return this.prisma.withDefaultTimeout(async (tx) => {
- const existing = await (tx as unknown as typeof this.prisma).intent.findUnique({
- where: { intentId: id },
- });
- if (!existing) return null;
- const row = existing as IntentRow;
- if (row.state !== fromState) return null;
- if (expectedVersion !== undefined && row.version !== expectedVersion) {
- return new VersionConflict(id, expectedVersion, row.version);
- }
- const updated = await (tx as unknown as typeof this.prisma).intent.updateMany({
- where: { intentId: id, state: fromState, version: row.version },
- data: { ...extra, state: toState, version: row.version + 1 },
- });
- if (updated.count === 0) return null;
- return rowToIntent({ ...row, ...extra, state: toState, version: row.version + 1 });
+ const result = await this.prisma.intent.updateMany({
+ where: {
+ intentId: id,
+ state: PrismaIntentState.accepted,
+ deadline: { lt: newDeadline },
+ ...(expectedVersion !== undefined ? { version: expectedVersion } : {}),
+ },
+ data: { deadline: newDeadline, version: { increment: 1 } },
});
+
+ if (result.count === 0) return this.resolveMiss(id, expectedVersion);
+
+ const row = await this.prisma.intent.findUnique({ where: { intentId: id } });
+ return row ? this.fromRow(row) : null;
}
+ // ── Private helpers ────────────────────────────────────────────────────────
+
/**
- * Resolve src/dst token FK columns from the tokens table (#410).
- * Returns the intent unchanged if either token is not in the registry.
+ * Classify a zero-row conditional update (issue #405): a stale
+ * `expectedVersion` becomes a {@link VersionConflict} carrying the version
+ * actually found so callers can re-read and retry; anything else (absent row
+ * or failed state guard) is `null`. Mirrors the in-memory adapter's
+ * ordering — the version check is reported before the state guard.
*/
- private async resolveTokenFks(intent: Intent): Promise {
- // Skip if already resolved.
- if (intent.srcTokenId && intent.dstTokenId) return intent;
+ private async resolveMiss(id: string, expectedVersion?: number): Promise {
+ if (expectedVersion === undefined) return null;
+ const row = await this.prisma.intent.findUnique({ where: { intentId: id } });
+ if (row && row.version !== expectedVersion) {
+ return new VersionConflict(id, expectedVersion, row.version);
+ }
+ return null;
+ }
- try {
- const [srcToken, dstToken] = await Promise.all([
- !intent.srcTokenId
- ? this.prisma.token.findUnique({
- where: {
- address_chain: {
- address: intent.srcToken.address,
- chain: intent.srcChain as string,
- },
- },
- select: { id: true, decimals: true },
- })
- : null,
- !intent.dstTokenId
- ? this.prisma.token.findUnique({
- where: {
- address_chain: {
- address: intent.dstToken.contract,
- chain: "stellar" as string,
- },
- },
- select: { id: true, decimals: true },
- })
- : null,
- ]);
-
- return {
- ...intent,
- srcTokenId: intent.srcTokenId ?? srcToken?.id ?? undefined,
- dstTokenId: intent.dstTokenId ?? dstToken?.id ?? undefined,
- srcDecimals: intent.srcDecimals ?? srcToken?.decimals ?? undefined,
- dstDecimals: intent.dstDecimals ?? dstToken?.decimals ?? undefined,
- };
- } catch (err) {
- this.logger.warn(`[#410] Failed to resolve token FKs for intent ${intent.intentId}: ${(err as Error).message}`);
- return intent;
+ /** Map Intent → Prisma create/update data (omits intentId which is the key). */
+ private toDbData(
+ intent: Intent,
+ ): Omit {
+ const data: Omit & { feeAmount?: string | null } = {
+ user: intent.user,
+ srcChain: intent.srcChain as Prisma.IntentCreateInput["srcChain"],
+ srcToken: intent.srcToken as unknown as Prisma.InputJsonValue,
+ srcAmount: intent.srcAmount,
+ dstToken: intent.dstToken as unknown as Prisma.InputJsonValue,
+ minDstAmount: intent.minDstAmount,
+ ...(intent.auction ? { auction: intent.auction as unknown as Prisma.InputJsonValue } : {}),
+ acceptedDstAmount: intent.acceptedDstAmount ?? null,
+ quotedDstAmount: intent.quotedDstAmount ?? null,
+ solver: intent.solver ?? null,
+ state: this.toPrismaState(intent.state),
+ createdAt: intent.createdAt,
+ deadline: intent.deadline,
+ filledAt: intent.filledAt ?? null,
+ fillAmount: intent.fillAmount ?? null,
+ txHash: intent.txHash ?? null,
+ ...(intent.usdValueAtCreate !== undefined
+ ? { usdValueAtCreate: intent.usdValueAtCreate }
+ : {}),
+ fillVerificationState: intent.fillVerificationState ?? null,
+ fillVerificationReason: intent.fillVerificationReason ?? null,
+ fillVerifiedAt: intent.fillVerifiedAt ? new Date(intent.fillVerifiedAt) : null,
+ version: intent.version ?? 0,
+ srcVerified: intent.srcVerified ?? false,
+ srcTxHash: intent.srcTxHash ?? null,
+ srcVerification: (intent.srcVerification ?? null) as unknown as Prisma.InputJsonValue,
+ ...(intent.slashedAt !== undefined ? { slashedAt: intent.slashedAt } : {}),
+ ...(intent.slashReason !== undefined ? { slashReason: intent.slashReason } : {}),
+ };
+
+ if (intent.feeAmount !== undefined) {
+ (data as { feeAmount?: string | null }).feeAmount = intent.feeAmount ?? null;
}
+
+ return data;
}
- /**
- * Convert a solver address string to a 64-bit integer for use as a Postgres
- * advisory lock key (per-solver serialization of exposure-cap check).
- *
- * We use a simple hash so the lock key stays within int8 range.
- */
- private solverAdvisoryLockKey(solver: string): bigint {
- let hash = 0n;
- for (let i = 0; i < solver.length; i++) {
- hash = (hash * 31n + BigInt(solver.charCodeAt(i))) & 0x7FFFFFFFFFFFFFFFn;
+ /** Build an `updateMany`-compatible data object from a partial Intent patch. */
+ private toDbPatch(patch: Partial