diff --git a/.github/workflows/backend-governance.yml b/.github/workflows/backend-governance.yml index cee52c55..58567cd6 100644 --- a/.github/workflows/backend-governance.yml +++ b/.github/workflows/backend-governance.yml @@ -49,6 +49,10 @@ jobs: cache: npm cache-dependency-path: backend/package-lock.json + - name: Build shared API schemas + run: npm ci && npm run build + working-directory: packages/api-schemas + - name: Install dependencies run: npm ci diff --git a/.github/workflows/rust-security.yml b/.github/workflows/rust-security.yml index 61ef5eda..da46133f 100644 --- a/.github/workflows/rust-security.yml +++ b/.github/workflows/rust-security.yml @@ -49,7 +49,7 @@ jobs: working-directory: frontend - name: Install cargo-audit - run: cargo install cargo-audit + run: cargo install cargo-audit --version 0.22.1 --locked - name: Run cargo audit run: | @@ -96,7 +96,7 @@ jobs: uses: dtolnay/rust-toolchain@nightly - name: Install cargo-fuzz - run: cargo install cargo-fuzz --locked + run: cargo +nightly install cargo-fuzz --locked - name: Run vault share-price fuzz (60s) run: | diff --git a/backend/package-lock.json b/backend/package-lock.json index 32f50aed..92e607e5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,11 +10,11 @@ "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.1058.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.218.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.221.0", "@opentelemetry/instrumentation-express": "^0.66.0", - "@opentelemetry/instrumentation-http": "^0.218.0", + "@opentelemetry/instrumentation-http": "^0.221.0", "@opentelemetry/resources": "^2.7.1", - "@opentelemetry/sdk-node": "^0.218.0", + "@opentelemetry/sdk-node": "^0.221.0", "@opentelemetry/semantic-conventions": "^1.41.1", "@prisma/client": "^5.10.0", "@prisma/instrumentation": "^7.8.0", @@ -57,6 +57,17 @@ "typescript": "^5.1.0" } }, + "../packages/api-schemas": { + "name": "@yieldvault/api-schemas", + "version": "1.0.0", + "dependencies": { + "zod": "^4.3.6" + }, + "devDependencies": { + "typescript": "~5.9.3", + "vitest": "^4.1.5" + } + }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", @@ -1961,9 +1972,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, "license": "MIT", "dependencies": { @@ -2473,13 +2484,13 @@ } }, "node_modules/@opentelemetry/configuration": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.218.0.tgz", - "integrity": "sha512-W8wIz7H2R1pufR5jfjb3gU2XkMpm2x/7b1RJcsuzvd70Il/rWWE+g5/Od7hQKrxRTSrTrOWlru101PWXz5I1EQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/configuration/-/configuration-0.221.0.tgz", + "integrity": "sha512-uE9y56Zdi9Gt/RdxYnVOo3YmFZkKJJMA0gqtBe8wh8gdtF5Asqe+Oh/TWiDtFb1s+31jNY4CWgnfIB1KOITfFA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "yaml": "^2.0.0" + "@opentelemetry/core": "2.10.0", + "yaml": "^2.8.3" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2489,9 +2500,9 @@ } }, "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.7.1.tgz", - "integrity": "sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.10.0.tgz", + "integrity": "sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==", "license": "Apache-2.0", "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2501,9 +2512,9 @@ } }, "node_modules/@opentelemetry/core": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", - "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2516,17 +2527,15 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.218.0.tgz", - "integrity": "sha512-hoxrNH1l/Xy6F9WTJ5IK+6j1r9nQFlPOmrnTlhYHTySdunfXLmUCPv3bQtKYntxag9h3wLYBZQ2HI6FOx+BT2g==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-txG1G0IrYSsKKMeiWZfj/i5cQmWB+h+hf3HzPpF3RqZVwp+iQQEIsv8Vtmzy6RWVdHdJZfygmVrBI39YTBvWcw==", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/sdk-logs": "0.218.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2536,16 +2545,14 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.218.0.tgz", - "integrity": "sha512-Qx+4rpVHzgg89dawcWRHyt+XRXeLnhFz/qBtvggmjkcgPUdr+NAB0/u/eIPA8yAeJV0J80Vz43JZCh/XFvZFGw==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.221.0.tgz", + "integrity": "sha512-nKXkr4Tomi6fjYVOf+ytcW3dZAVr4v4Bv5gsT6dr2gvpUPJpKgHB4XbMufMsPotRE3g0XH2GwVVCkN2w6SON+Q==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/sdk-logs": "0.218.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2555,18 +2562,14 @@ } }, "node_modules/@opentelemetry/exporter-logs-otlp-proto": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.218.0.tgz", - "integrity": "sha512-1/noQNsp9gXD75HPzgjBrcF1+XTtry7pFAUfxVEJgg7mPv2AawKQuYkhMmJ8qjxz4Ubc3Y8bwvfxevXsKTq4cg==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.221.0.tgz", + "integrity": "sha512-AH6EY+47gXFaWYgG3hfeOneGiE9xIZGtDBk+9g0sM8NZWzsQhhmqPbQQXJzS7pyCh5jRRr2nYNXVrkCmoojRvQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2576,19 +2579,14 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.218.0.tgz", - "integrity": "sha512-YapQ9vNMX0NSZF6LK5pWAFfjpJleV2O9uYWfYGeb/5F1Kb9rPGK8tZDMJFa/sOksgdFuflDvYuA0B4qjDB4fjQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-KOgCtO15FC6C1T/xOqBcr7EyUs7B+7yomGNb5Y97d3s38rPbCCk5sewkmE2b0/itOkQ/PptX8CLlD+kn2mEtTg==", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2598,16 +2596,16 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.218.0.tgz", - "integrity": "sha512-bV7d2OuMpZu2+gAaxUAhzfZ0h3WVZk8ETQUEE3DNSntbTaMpuITjtm8I0rNyHFdm7Ax57K6ty7SgFXlBmOLIvQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.221.0.tgz", + "integrity": "sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2617,17 +2615,14 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.218.0.tgz", - "integrity": "sha512-ubLddKjWULhla9YZRCj/rTBeppjJYE4e9w0icx5mTu3eFhWjQzbV75NYjXuIlEG+NJsBl6d+sTFw5Qu+oej4oQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.221.0.tgz", + "integrity": "sha512-YMF4LveY2I3yhw61rn6nmC9FE8U24IZHPeKU1Duc5+sbwjMd8FwZAwba318ImdThCg/HuVQvhm2y6bfgNPnfYg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2637,14 +2632,14 @@ } }, "node_modules/@opentelemetry/exporter-prometheus": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.218.0.tgz", - "integrity": "sha512-RT5oEyu1kddZJ1vt7/BUo5wV+P7hpNAESsR3dUd3+8deHuX7gWNoCOZn+SfDT+hJHlIJ5h/AxiCLXIrutswDJg==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.221.0.tgz", + "integrity": "sha512-kW79a20qWESIuAdDrxzg9WKM98twV/NBWBFRAH57ap/+ssZhiCo0hckzKT0zpuwR/gSHrFAQhJL0bYDrnEM34g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -2655,18 +2650,15 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.218.0.tgz", - "integrity": "sha512-3fXxVQEj9TNAFaCi79JeFKfeLd0sDtInaR3gaZDVlzNSPHtz8PZuCV34JKWjD4XXzT20IdMe8IpX6mRVNDA4Tw==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-zXminlZedtq9LvOW64CnNkOqk15zV75k8JgtdTuWFge6+jk2m4GmAUm6L2eIiG1o2a2bZxXw2PDrszm+bps0IA==", "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2676,16 +2668,14 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.218.0.tgz", - "integrity": "sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.221.0.tgz", + "integrity": "sha512-AySXiKoC+meiWm6zdVj5T2LnPDZuatveBby1cMOeQteIWsYXAUxs8Sru13G2pVSPrUXz6vF+og7QVBX6GdC/oQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2695,16 +2685,14 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.218.0.tgz", - "integrity": "sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.221.0.tgz", + "integrity": "sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2714,14 +2702,14 @@ } }, "node_modules/@opentelemetry/exporter-zipkin": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.7.1.tgz", - "integrity": "sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.10.0.tgz", + "integrity": "sha512-7gsvgf0UDoJ4l9ObrwBmz5G/ZogiPk+lq+g5GpLp24YQF/vPM/BSsnOfcLnfinast5ASUgLo78uSC/ObjlnXgg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -2766,13 +2754,13 @@ } }, "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.218.0.tgz", - "integrity": "sha512-x9djaqdzpT8WAboep1H9nCAQ1E+MMsm08TNfA02TqM3bNNddZeiim+E3KMWVQFaX6JpUy7V0nm/wfN/K2Em+Zw==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.221.0.tgz", + "integrity": "sha512-oIP91CPIANuYr09tGFElPFKAh6JUar+awJf1kBRYlaeo9b0gDwZHEB2zBfFlvdNFHm0wAVutMZODVi5smKT30g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/instrumentation": "0.218.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/instrumentation": "0.221.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, @@ -2783,14 +2771,43 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/instrumentation": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.221.0.tgz", + "integrity": "sha512-cCk80Z/iRDf/5gfsKMB4f74LqVA5yKETB/9ojPzVW/6/f70iu89nJvGxsFCxx4XfSohaOofkU19kiYm84AiAlw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2800,15 +2817,15 @@ } }, "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.218.0.tgz", - "integrity": "sha512-H/lCGJ536N98VpYJOaWTQOkv4Dx6TnmStK6Rqfu1W7KkFbPAx04hjdYEMZF/YbnHzPUSIK4kM6OE2GKGBTpV9A==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.221.0.tgz", + "integrity": "sha512-rQDmNgyiGCTrescjnzH2ntVyUKVIq6I2UjuK8+stT/Xg0ZOT71FVJqwjFdspQl6Yol/Yqsut9bDo+ame8oTmDQ==", "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2818,17 +2835,17 @@ } }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2837,13 +2854,25 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/propagator-b3": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.7.1.tgz", - "integrity": "sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-2.10.0.tgz", + "integrity": "sha512-GnA5B24H+1w8BO21J0q+IWNB0z1v+AGbcquTdIt/dufibhnhgxaA8YKvz0I3akRZhB1jHT+/tlzK+qlAjEDybQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1" + "@opentelemetry/core": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2853,12 +2882,12 @@ } }, "node_modules/@opentelemetry/propagator-jaeger": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.7.1.tgz", - "integrity": "sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.10.0.tgz", + "integrity": "sha512-yw/IX8DL470dSMZJoE82ScfYGp7JWZ/G8kFJo35ZILUVTB2jFPTOaioN+8s09pH0RHsWNhweVZb+ZnjJJpCChg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1" + "@opentelemetry/core": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2868,12 +2897,12 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", + "@opentelemetry/core": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -2884,14 +2913,14 @@ } }, "node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -2901,14 +2930,26 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -2918,35 +2959,83 @@ } }, "node_modules/@opentelemetry/sdk-node": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.218.0.tgz", - "integrity": "sha512-tPMjHrLV5gsfNdYqoRHjeGbCAZBXXD9c1Qo/2ut7VwnUABDNh76xNxrT0SEhkIIJuCN45bbN1vZnYL1gY0IkOg==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.221.0.tgz", + "integrity": "sha512-UbYuvtBrQQB5Prsh9KOKy4kxzexFxfMs5MkteHeWMoswsEB7kiNhyUVkAOFW/qsEzNHtrkgyghrD2ilZJa+5YA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/configuration": "0.218.0", - "@opentelemetry/context-async-hooks": "2.7.1", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/exporter-logs-otlp-grpc": "0.218.0", - "@opentelemetry/exporter-logs-otlp-http": "0.218.0", - "@opentelemetry/exporter-logs-otlp-proto": "0.218.0", - "@opentelemetry/exporter-metrics-otlp-grpc": "0.218.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", - "@opentelemetry/exporter-metrics-otlp-proto": "0.218.0", - "@opentelemetry/exporter-prometheus": "0.218.0", - "@opentelemetry/exporter-trace-otlp-grpc": "0.218.0", - "@opentelemetry/exporter-trace-otlp-http": "0.218.0", - "@opentelemetry/exporter-trace-otlp-proto": "0.218.0", - "@opentelemetry/exporter-zipkin": "2.7.1", - "@opentelemetry/instrumentation": "0.218.0", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/propagator-b3": "2.7.1", - "@opentelemetry/propagator-jaeger": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1", - "@opentelemetry/sdk-trace-node": "2.7.1", + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/configuration": "0.221.0", + "@opentelemetry/context-async-hooks": "2.10.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-logs-otlp-http": "0.221.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.221.0", + "@opentelemetry/exporter-prometheus": "0.221.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-trace-otlp-http": "0.221.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.221.0", + "@opentelemetry/exporter-zipkin": "2.10.0", + "@opentelemetry/instrumentation": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/propagator-b3": "2.10.0", + "@opentelemetry/propagator-jaeger": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0", + "@opentelemetry/sdk-trace-node": "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-node/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/instrumentation": { + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.221.0.tgz", + "integrity": "sha512-cCk80Z/iRDf/5gfsKMB4f74LqVA5yKETB/9ojPzVW/6/f70iu89nJvGxsFCxx4XfSohaOofkU19kiYm84AiAlw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.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": { @@ -2957,13 +3046,14 @@ } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "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.7.1", - "@opentelemetry/resources": "2.7.1", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -2974,14 +3064,14 @@ } }, "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.7.1.tgz", - "integrity": "sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==", + "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.7.1", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" + "@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" @@ -3182,9 +3272,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "license": "BSD-3-Clause" }, "node_modules/@scarf/scarf": { @@ -3966,11 +4056,8 @@ "license": "ISC" }, "node_modules/@yieldvault/api-schemas": { - "version": "1.0.0", - "resolved": "file:../packages/api-schemas", - "dependencies": { - "zod": "^4.3.6" - } + "resolved": "../packages/api-schemas", + "link": true }, "node_modules/accepts": { "version": "1.3.8", @@ -4163,13 +4250,13 @@ } }, "node_modules/axios": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", - "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -4409,9 +4496,9 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -4454,9 +4541,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -5729,9 +5816,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", "funding": [ { "type": "github", @@ -7354,9 +7441,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "funding": [ { "type": "github", @@ -8419,9 +8506,9 @@ } }, "node_modules/protobufjs": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", - "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -9285,15 +9372,15 @@ } }, "node_modules/swagger-jsdoc/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/swagger-jsdoc/node_modules/glob": { diff --git a/backend/package.json b/backend/package.json index a592bcba..a223ae15 100644 --- a/backend/package.json +++ b/backend/package.json @@ -30,18 +30,18 @@ "author": "", "license": "MIT", "dependencies": { - "@yieldvault/api-schemas": "file:../packages/api-schemas", "@aws-sdk/client-s3": "^3.1058.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.218.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.221.0", "@opentelemetry/instrumentation-express": "^0.66.0", - "@opentelemetry/instrumentation-http": "^0.218.0", + "@opentelemetry/instrumentation-http": "^0.221.0", "@opentelemetry/resources": "^2.7.1", - "@opentelemetry/sdk-node": "^0.218.0", + "@opentelemetry/sdk-node": "^0.221.0", "@opentelemetry/semantic-conventions": "^1.41.1", "@prisma/client": "^5.10.0", "@prisma/instrumentation": "^7.8.0", "@stellar/stellar-base": "^13.1.0", "@stellar/stellar-sdk": "^13.0.0", + "@yieldvault/api-schemas": "file:../packages/api-schemas", "cors": "^2.8.6", "decimal.js": "^10.6.0", "dotenv": "^16.3.1", diff --git a/backend/prisma/dev.db b/backend/prisma/dev.db index b94640da..e7010fdf 100644 Binary files a/backend/prisma/dev.db and b/backend/prisma/dev.db differ diff --git a/backend/prisma/migrations/20260824204600_add_event_outbox/migration.sql b/backend/prisma/migrations/20260824204600_add_event_outbox/migration.sql new file mode 100644 index 00000000..35e61951 --- /dev/null +++ b/backend/prisma/migrations/20260824204600_add_event_outbox/migration.sql @@ -0,0 +1,88 @@ +-- CreateTable +CREATE TABLE "AdminConfigChange" ( + "id" TEXT NOT NULL PRIMARY KEY, + "configType" TEXT NOT NULL, + "action" TEXT NOT NULL, + "actor" TEXT NOT NULL, + "ipAddress" TEXT, + "userAgent" TEXT, + "preChangeSnapshot" TEXT NOT NULL, + "postChangeSnapshot" TEXT NOT NULL, + "metadata" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "FeatureFlagOverride" ( + "id" TEXT NOT NULL PRIMARY KEY, + "flagName" TEXT NOT NULL, + "enabled" BOOLEAN NOT NULL, + "scopeType" TEXT NOT NULL, + "scopeValue" TEXT, + "expiresAt" DATETIME NOT NULL, + "actor" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateTable +CREATE TABLE "EventOutbox" ( + "id" TEXT NOT NULL PRIMARY KEY, + "eventType" TEXT NOT NULL, + "payload" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "aggregateType" TEXT NOT NULL, + "aggregateId" TEXT NOT NULL, + "attemptCount" INTEGER NOT NULL DEFAULT 0, + "maxAttempts" INTEGER NOT NULL DEFAULT 3, + "lastError" TEXT, + "lockedAt" DATETIME, + "lockedBy" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "relayedAt" DATETIME +); + +-- AlterTable: add optimistic-concurrency version column. +-- A plain ADD COLUMN with a constant DEFAULT is fully supported by SQLite +-- and preserves the existing table (and its indexes) in place, unlike +-- Prisma's default drop-and-rebuild diff for SQLite. +ALTER TABLE "BulkExportJob" ADD COLUMN "version" INTEGER DEFAULT 1 NOT NULL; + +-- AlterTable: add optimistic-concurrency version column (see above). +ALTER TABLE "VaultState" ADD COLUMN "version" INTEGER DEFAULT 1 NOT NULL; + +-- CreateIndex +CREATE INDEX "AdminConfigChange_configType_idx" ON "AdminConfigChange"("configType"); + +-- CreateIndex +CREATE INDEX "AdminConfigChange_createdAt_idx" ON "AdminConfigChange"("createdAt"); + +-- CreateIndex +CREATE INDEX "AdminConfigChange_actor_idx" ON "AdminConfigChange"("actor"); + +-- CreateIndex +CREATE INDEX "FeatureFlagOverride_flagName_idx" ON "FeatureFlagOverride"("flagName"); + +-- CreateIndex +CREATE INDEX "FeatureFlagOverride_scopeType_scopeValue_idx" ON "FeatureFlagOverride"("scopeType", "scopeValue"); + +-- CreateIndex +CREATE INDEX "FeatureFlagOverride_expiresAt_idx" ON "FeatureFlagOverride"("expiresAt"); + +-- CreateIndex +CREATE INDEX "FeatureFlagOverride_actor_idx" ON "FeatureFlagOverride"("actor"); + +-- CreateIndex +CREATE INDEX "EventOutbox_status_idx" ON "EventOutbox"("status"); + +-- CreateIndex +CREATE INDEX "EventOutbox_status_createdAt_idx" ON "EventOutbox"("status", "createdAt"); + +-- CreateIndex +CREATE INDEX "EventOutbox_aggregateType_aggregateId_idx" ON "EventOutbox"("aggregateType", "aggregateId"); + +-- CreateIndex +CREATE INDEX "EventOutbox_lockedAt_idx" ON "EventOutbox"("lockedAt"); + +-- CreateIndex +CREATE INDEX "EventOutbox_createdAt_idx" ON "EventOutbox"("createdAt"); diff --git a/backend/schema-snapshots/get-_api_v1_vault_summary.json b/backend/schema-snapshots/get-_api_v1_vault_summary.json index 090a1be9..f261b9c7 100644 --- a/backend/schema-snapshots/get-_api_v1_vault_summary.json +++ b/backend/schema-snapshots/get-_api_v1_vault_summary.json @@ -2,10 +2,13 @@ "type": "object", "properties": { "totalAssets": { - "type": "number" + "type": "string" }, "totalShares": { - "type": "number" + "type": "string" + }, + "sharePrice": { + "type": "string" }, "apy": { "type": "number" @@ -17,6 +20,7 @@ "required": [ "totalAssets", "totalShares", + "sharePrice", "apy", "timestamp" ], diff --git a/backend/schema-snapshots/get-_health.json b/backend/schema-snapshots/get-_health.json index f0dd64c6..31d8f6b4 100644 --- a/backend/schema-snapshots/get-_health.json +++ b/backend/schema-snapshots/get-_health.json @@ -13,6 +13,9 @@ "environment": { "type": "string" }, + "lastIndexedLedger": { + "type": "number" + }, "checks": { "type": "object", "properties": { @@ -117,6 +120,7 @@ "timestamp", "uptime", "environment", + "lastIndexedLedger", "checks", "sorobanCircuitBreaker" ], diff --git a/backend/scripts/check-migrations.js b/backend/scripts/check-migrations.js index 727bb567..a087d9b7 100644 --- a/backend/scripts/check-migrations.js +++ b/backend/scripts/check-migrations.js @@ -80,14 +80,17 @@ function checkFile(file) { // Safe opt-out: add `-- migration-safety: canary-safe` at the top of the // file to acknowledge the risk (e.g. zero-downtime step 2 of 3). // - // The regex looks for ADD COLUMN ... NOT NULL within a 300-char window and - // checks that DEFAULT does NOT appear in that window. - const addColumnNotNullPattern = /\badd\s+column\b[^;]{0,300}\bnot\s+null\b/gi; + // The regex captures the whole ADD COLUMN statement (up to 300 chars or the + // next semicolon) and checks that whole window for NOT NULL and DEFAULT, + // since either can come first — "NOT NULL DEFAULT x" (Prisma's own + // convention) and "DEFAULT x NOT NULL" are equally valid SQL. + const addColumnStatementPattern = /\badd\s+column\b[^;]{0,300}/gi; let match; - while ((match = addColumnNotNullPattern.exec(content)) !== null) { + while ((match = addColumnStatementPattern.exec(content)) !== null) { const snippet = match[0]; + const hasNotNull = /\bnot\s+null\b/i.test(snippet); const hasDefault = /\bdefault\b/i.test(snippet); - if (!hasDefault && !isCanarySafeOptIn) { + if (hasNotNull && !hasDefault && !isCanarySafeOptIn) { results.push({ file, severity: 'error', diff --git a/backend/src/__tests__/adminFeatures.test.ts b/backend/src/__tests__/adminFeatures.test.ts index 34447ffc..ff0343ab 100644 --- a/backend/src/__tests__/adminFeatures.test.ts +++ b/backend/src/__tests__/adminFeatures.test.ts @@ -5,6 +5,7 @@ import { resetWebhookState } from '../webhookDelivery'; import { resetAuditLogs } from '../auditLog'; import { resetTransactionBackfillJobsForTests } from '../transactionBackfill'; import { resetExportManifestsForTests } from '../exportManifest'; +import { eventOutboxService } from '../eventOutbox'; describe('Admin backend features', () => { const adminKey = 'admin-feature-test-key'; @@ -52,7 +53,7 @@ describe('Admin backend features', () => { .send({ amount: '125.00', asset: 'USDC', - walletAddress: 'GABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz234567', + walletAddress: 'G234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQ', }); if (typeof previousAllowlistEnabled === 'string') { @@ -63,7 +64,11 @@ describe('Admin backend features', () => { expect(depositResponse.status).toBe(201); + // The deposit handler writes the event to the outbox fire-and-forget, so + // wait for that write to land before explicitly draining it — the + // background poller isn't guaranteed to run within a fixed test delay. await new Promise((resolve) => setTimeout(resolve, 30)); + await eventOutboxService.processOutbox(10); const deliveriesResponse = await request(app) .get('/admin/webhooks/deliveries') diff --git a/backend/src/__tests__/api.test.ts b/backend/src/__tests__/api.test.ts index 3fdb5c4b..9b5cc5cc 100644 --- a/backend/src/__tests__/api.test.ts +++ b/backend/src/__tests__/api.test.ts @@ -1,3 +1,11 @@ +// This file has its own dedicated adaptive-throttle escalation test and +// already resets that middleware's state before every test (below), so it +// needs the real threshold restored here — setup.ts globally raises +// ADAPTIVE_THROTTLE_SCORE_THRESHOLD for every other integration test file, +// most of which have no such per-test reset. Must run before importing +// '../index', which reads this value once at module-load time. +process.env.ADAPTIVE_THROTTLE_SCORE_THRESHOLD = '6'; + import request from 'supertest'; import app from '../index'; import { resetAdaptiveThrottleStateForTests } from '../middleware/adaptiveThrottle'; @@ -141,12 +149,21 @@ describe('Backend API', () => { it('should adaptively throttle repeated 4xx abuse patterns per IP', async () => { const clientIp = '198.51.100.33'; - for (let i = 0; i < 8; i++) { + // 404s are scored at half weight (0.5) vs. other 4xx (1) in + // scoreForStatus — deliberately less suspicious than e.g. repeated + // 401/403s — so crossing the default threshold of 6 needs at least 12 + // hits, not 8. + for (let i = 0; i < 20; i++) { await request(app) .get('/api/not-found-abuse') .set('x-forwarded-for', clientIp); } + // The middleware scores each response in a fire-and-forget res.on('finish') + // handler, not before the response is sent, so the priming loop above can + // outrun its own bookkeeping. Give it a beat to settle before relying on it. + await new Promise((resolve) => setTimeout(resolve, 100)); + const throttled = await request(app) .get('/api/not-found-abuse') .set('x-forwarded-for', clientIp); diff --git a/backend/src/__tests__/eventOutbox.test.ts b/backend/src/__tests__/eventOutbox.test.ts index 81607318..0949eff3 100644 --- a/backend/src/__tests__/eventOutbox.test.ts +++ b/backend/src/__tests__/eventOutbox.test.ts @@ -448,7 +448,7 @@ describe('EventOutboxService', () => { describe('end-to-end flow', () => { it('completes the full outbox lifecycle', async () => { - let deliveredPayloads: unknown[] = []; + const deliveredPayloads: unknown[] = []; global.fetch = jest.fn(async (_url, init) => { if (init?.body && String(init.body).includes('webhook.verification')) { const body = JSON.parse(String(init.body)); diff --git a/backend/src/__tests__/eventPollingService.test.ts b/backend/src/__tests__/eventPollingService.test.ts index fceae8fb..a287eb4c 100644 --- a/backend/src/__tests__/eventPollingService.test.ts +++ b/backend/src/__tests__/eventPollingService.test.ts @@ -329,6 +329,50 @@ describe('EventPollingService', () => { expect(true).toBe(true); }); + it('starts the continuous polling loop even when startup replay fails outright', async () => { + (prisma.eventCursor.findUnique as jest.Mock).mockResolvedValue({ + id: 1, + lastLedgerSeq: 1000, + }); + + // getLatestLedger succeeds (there are missed events to replay), but the + // subsequent getEvents call for the replay fails outright — simulating + // an RPC outage mid-replay rather than a clean "nothing missed" case. + (fetch as jest.Mock).mockImplementation((_url, options) => { + const body = JSON.parse((options as RequestInit).body as string); + if (body.method === 'getLatestLedger') { + return Promise.resolve({ + json: async () => ({ result: { sequence: 1050 } }), + }); + } + return Promise.reject(new Error('RPC unavailable')); + }); + + // start() must not reject: a failed replay should degrade gracefully + // instead of aborting before the continuous polling loop is armed. + await expect(service.start()).resolves.toBeUndefined(); + + // The continuous polling loop should now be armed for this leader — + // subsequent successful polls pick up where the failed replay left off. + (fetch as jest.Mock).mockReset(); + (fetch as jest.Mock).mockImplementation((_url, options) => { + const body = JSON.parse((options as RequestInit).body as string); + if (body.method === 'getLatestLedger') { + return Promise.resolve({ json: async () => ({ result: { sequence: 1050 } }) }); + } + return Promise.resolve({ json: async () => ({ result: { events: [] } }) }); + }); + (prisma.eventCursor.upsert as jest.Mock).mockResolvedValue({}); + + await (service as any).pollEvents(); + + expect(prisma.eventCursor.upsert).toHaveBeenCalledWith({ + where: { id: 1 }, + update: { lastLedgerSeq: 1050 }, + create: { id: 1, lastLedgerSeq: 1050 }, + }); + }); + it('should continue polling after transient errors', async () => { (prisma.eventCursor.findUnique as jest.Mock).mockResolvedValue({ id: 1, diff --git a/backend/src/__tests__/issues705-707-719-723.test.ts b/backend/src/__tests__/issues705-707-719-723.test.ts index f12375d7..b35612de 100644 --- a/backend/src/__tests__/issues705-707-719-723.test.ts +++ b/backend/src/__tests__/issues705-707-719-723.test.ts @@ -408,12 +408,12 @@ describe('Issue #719: Health probe decomposition for dependencies', () => { // ─── Issue #723: Permission-scoped admin tokens ───────────────────────────── describe('Issue #723: Permission-scoped admin tokens with rotating key identifiers', () => { - beforeEach(() => { - scopedAdminTokenStore.clear(); + beforeEach(async () => { + await scopedAdminTokenStore.clear(); }); - it('creates a scoped token with permissions', () => { - const { token, secret } = scopedAdminTokenStore.create({ + it('creates a scoped token with permissions', async () => { + const { token, secret } = await scopedAdminTokenStore.create({ label: 'CI Pipeline', permissions: ['read:metrics', 'read:audit'], createdBy: 'admin-1', @@ -427,56 +427,56 @@ describe('Issue #723: Permission-scoped admin tokens with rotating key identifie expect(secret.length).toBe(64); }); - it('authenticates a valid token', () => { - const { token, secret } = scopedAdminTokenStore.create({ + it('authenticates a valid token', async () => { + const { token, secret } = await scopedAdminTokenStore.create({ label: 'Test Token', permissions: ['read:metrics'], createdBy: 'admin-1', }); - const authenticated = scopedAdminTokenStore.authenticate(token.keyId, secret); + const authenticated = await scopedAdminTokenStore.authenticate(token.keyId, secret); expect(authenticated).not.toBeNull(); expect(authenticated!.keyId).toBe(token.keyId); }); - it('rejects authentication with wrong secret', () => { - const { token } = scopedAdminTokenStore.create({ + it('rejects authentication with wrong secret', async () => { + const { token } = await scopedAdminTokenStore.create({ label: 'Test Token', permissions: ['read:metrics'], createdBy: 'admin-1', }); - const result = scopedAdminTokenStore.authenticate(token.keyId, 'wrong-secret'); + const result = await scopedAdminTokenStore.authenticate(token.keyId, 'wrong-secret'); expect(result).toBeNull(); }); - it('rejects authentication for revoked tokens', () => { - const { token, secret } = scopedAdminTokenStore.create({ + it('rejects authentication for revoked tokens', async () => { + const { token, secret } = await scopedAdminTokenStore.create({ label: 'Revokable', permissions: ['read:metrics'], createdBy: 'admin-1', }); - scopedAdminTokenStore.revoke(token.keyId); + await scopedAdminTokenStore.revoke(token.keyId); - const result = scopedAdminTokenStore.authenticate(token.keyId, secret); + const result = await scopedAdminTokenStore.authenticate(token.keyId, secret); expect(result).toBeNull(); }); - it('rejects authentication for expired tokens', () => { - const { token, secret } = scopedAdminTokenStore.create({ + it('rejects authentication for expired tokens', async () => { + const { token, secret } = await scopedAdminTokenStore.create({ label: 'Short-lived', permissions: ['read:metrics'], expiresInSeconds: -1, createdBy: 'admin-1', }); - const result = scopedAdminTokenStore.authenticate(token.keyId, secret); + const result = await scopedAdminTokenStore.authenticate(token.keyId, secret); expect(result).toBeNull(); }); - it('checks individual permissions', () => { - const { token } = scopedAdminTokenStore.create({ + it('checks individual permissions', async () => { + const { token } = await scopedAdminTokenStore.create({ label: 'Partial', permissions: ['read:metrics', 'read:audit'], createdBy: 'admin-1', @@ -487,8 +487,8 @@ describe('Issue #723: Permission-scoped admin tokens with rotating key identifie expect(scopedAdminTokenStore.hasPermission(token, 'write:config')).toBe(false); }); - it('admin:* permission grants access to all permissions', () => { - const { token } = scopedAdminTokenStore.create({ + it('admin:* permission grants access to all permissions', async () => { + const { token } = await scopedAdminTokenStore.create({ label: 'Super', permissions: ['admin:*'], createdBy: 'admin-1', @@ -499,74 +499,74 @@ describe('Issue #723: Permission-scoped admin tokens with rotating key identifie expect(scopedAdminTokenStore.hasPermission(token, 'write:maintenance')).toBe(true); }); - it('rotates token secret', () => { - const { token, secret: oldSecret } = scopedAdminTokenStore.create({ + it('rotates token secret', async () => { + const { token, secret: oldSecret } = await scopedAdminTokenStore.create({ label: 'Rotatable', permissions: ['read:metrics'], createdBy: 'admin-1', }); - const result = scopedAdminTokenStore.rotate(token.keyId); + const result = await scopedAdminTokenStore.rotate(token.keyId); expect(result).not.toBeNull(); expect(result!.newSecret).not.toBe(oldSecret); expect(result!.rotatedAt).toBeDefined(); // Old secret no longer works - expect(scopedAdminTokenStore.authenticate(token.keyId, oldSecret)).toBeNull(); + expect(await scopedAdminTokenStore.authenticate(token.keyId, oldSecret)).toBeNull(); // New secret works - expect(scopedAdminTokenStore.authenticate(token.keyId, result!.newSecret)).not.toBeNull(); + expect(await scopedAdminTokenStore.authenticate(token.keyId, result!.newSecret)).not.toBeNull(); }); - it('cannot rotate revoked token', () => { - const { token } = scopedAdminTokenStore.create({ + it('cannot rotate revoked token', async () => { + const { token } = await scopedAdminTokenStore.create({ label: 'Revoked', permissions: ['read:metrics'], createdBy: 'admin-1', }); - scopedAdminTokenStore.revoke(token.keyId); - const result = scopedAdminTokenStore.rotate(token.keyId); + await scopedAdminTokenStore.revoke(token.keyId); + const result = await scopedAdminTokenStore.rotate(token.keyId); expect(result).toBeNull(); }); - it('lists active tokens excluding revoked by default', () => { - scopedAdminTokenStore.create({ + it('lists active tokens excluding revoked by default', async () => { + await scopedAdminTokenStore.create({ label: 'Active', permissions: ['read:metrics'], createdBy: 'admin-1', }); - const { token: revoked } = scopedAdminTokenStore.create({ + const { token: revoked } = await scopedAdminTokenStore.create({ label: 'Revoked', permissions: ['read:audit'], createdBy: 'admin-1', }); - scopedAdminTokenStore.revoke(revoked.keyId); + await scopedAdminTokenStore.revoke(revoked.keyId); - const activeOnly = scopedAdminTokenStore.list(); + const activeOnly = await scopedAdminTokenStore.list(); expect(activeOnly).toHaveLength(1); - const all = scopedAdminTokenStore.list({ includeRevoked: true }); + const all = await scopedAdminTokenStore.list({ includeRevoked: true }); expect(all).toHaveLength(2); }); - it('rejects invalid permissions', () => { - expect(() => + it('rejects invalid permissions', async () => { + await expect( scopedAdminTokenStore.create({ label: 'Invalid', permissions: ['invalid:perm' as any], createdBy: 'admin-1', }), - ).toThrow('Invalid permission'); + ).rejects.toThrow('Invalid permission'); }); - it('rejects empty permissions array', () => { - expect(() => + it('rejects empty permissions array', async () => { + await expect( scopedAdminTokenStore.create({ label: 'Empty', permissions: [], createdBy: 'admin-1', }), - ).toThrow('At least one permission is required'); + ).rejects.toThrow('At least one permission is required'); }); it('returns valid permissions list', () => { @@ -577,8 +577,8 @@ describe('Issue #723: Permission-scoped admin tokens with rotating key identifie expect(perms.length).toBeGreaterThan(5); }); - it('hasAnyPermission checks multiple permissions', () => { - const { token } = scopedAdminTokenStore.create({ + it('hasAnyPermission checks multiple permissions', async () => { + const { token } = await scopedAdminTokenStore.create({ label: 'Multi', permissions: ['read:metrics'], createdBy: 'admin-1', diff --git a/backend/src/__tests__/issues711.test.ts b/backend/src/__tests__/issues711.test.ts index 2700516a..6bdaa11d 100644 --- a/backend/src/__tests__/issues711.test.ts +++ b/backend/src/__tests__/issues711.test.ts @@ -52,6 +52,7 @@ describe('#711 API contract schema snapshots', () => { timestamp: new Date().toISOString(), uptime: 12.5, environment: 'test', + lastIndexedLedger: 0, checks: { api: 'up', cache: 'up', @@ -82,8 +83,9 @@ describe('#711 API contract schema snapshots', () => { it('validates a conforming vault summary payload', () => { const result = validateResponseAgainstSchema('GET /api/v1/vault/summary', { - totalAssets: 1000, - totalShares: 500, + totalAssets: '1000', + totalShares: '500', + sharePrice: '1.000000', apy: 8.5, timestamp: new Date().toISOString(), }); diff --git a/backend/src/__tests__/jobGovernanceMetrics.test.ts b/backend/src/__tests__/jobGovernanceMetrics.test.ts index 414411ec..06e72188 100644 --- a/backend/src/__tests__/jobGovernanceMetrics.test.ts +++ b/backend/src/__tests__/jobGovernanceMetrics.test.ts @@ -24,7 +24,7 @@ describe('syncJobGovernanceMetrics', () => { syncJobGovernanceMetrics(); const metricsText = await register.metrics(); - expect(metricsText).toMatch(/job_dead_letter_count\{job_name="priceRefresh"\}\s+1/); + expect(metricsText).toMatch(/job_dead_letter_count\{[^}]*job_name="priceRefresh"[^}]*\}\s+1/); }); it('sets job_health_status=0 for jobs with recurring failures', async () => { @@ -44,7 +44,7 @@ describe('syncJobGovernanceMetrics', () => { syncJobGovernanceMetrics(); const metricsText = await register.metrics(); - expect(metricsText).toMatch(/job_health_status\{job_name="positionReconciliation"\}\s+0/); + expect(metricsText).toMatch(/job_health_status\{[^}]*job_name="positionReconciliation"[^}]*\}\s+0/); }); it('sets job_health_status=1 for jobs below recurring failure threshold', async () => { @@ -62,6 +62,6 @@ describe('syncJobGovernanceMetrics', () => { // 1 failure < threshold of 3, so health should still be up (1) const metricsText = await register.metrics(); - expect(metricsText).toMatch(/job_health_status\{job_name="priceRefresh"\}\s+1/); + expect(metricsText).toMatch(/job_health_status\{[^}]*job_name="priceRefresh"[^}]*\}\s+1/); }); }); diff --git a/backend/src/__tests__/openApiContractTests.test.ts b/backend/src/__tests__/openApiContractTests.test.ts index f38946e7..a280e737 100644 --- a/backend/src/__tests__/openApiContractTests.test.ts +++ b/backend/src/__tests__/openApiContractTests.test.ts @@ -146,8 +146,11 @@ describe('OpenAPI contract: GET /api/v1/vault/summary', () => { it('numeric fields are finite numbers', async () => { const res = await request(app).get('/api/v1/vault/summary'); - expect(Number.isFinite(res.body.totalAssets)).toBe(true); - expect(Number.isFinite(res.body.totalShares)).toBe(true); + // totalAssets/totalShares/sharePrice are Decimal-backed strings (to avoid + // floating-point precision loss), not numbers — verify they parse cleanly. + expect(Number.isFinite(Number(res.body.totalAssets))).toBe(true); + expect(Number.isFinite(Number(res.body.totalShares))).toBe(true); + expect(Number.isFinite(Number(res.body.sharePrice))).toBe(true); expect(Number.isFinite(res.body.apy)).toBe(true); }); @@ -159,7 +162,7 @@ describe('OpenAPI contract: GET /api/v1/vault/summary', () => { it('does not contain unexpected extra fields (additionalProperties: false)', async () => { const res = await request(app).get('/api/v1/vault/summary'); - const allowedKeys = new Set(['totalAssets', 'totalShares', 'apy', 'timestamp']); + const allowedKeys = new Set(['totalAssets', 'totalShares', 'sharePrice', 'apy', 'timestamp']); for (const key of Object.keys(res.body)) { expect(allowedKeys.has(key)).toBe(true); } diff --git a/backend/src/__tests__/rateLimiter.auth_transfer.test.ts b/backend/src/__tests__/rateLimiter.auth_transfer.test.ts index 245fac21..bfef8e68 100644 --- a/backend/src/__tests__/rateLimiter.auth_transfer.test.ts +++ b/backend/src/__tests__/rateLimiter.auth_transfer.test.ts @@ -3,6 +3,14 @@ * Integration tests for request-level rate limiting on Auth and Transfer APIs (#887). */ +// This file tests the deposits tier's real, tight default (10 req/min), so it +// must restore that default here — setup.ts globally raises +// DEPOSITS_RATE_LIMIT_MAX for every other integration test file, since most +// of them just exercise deposit/withdrawal routes incidentally and aren't +// testing rate limiting at all. Must run before importing rateLimiter, whose +// singleton limiters read this value once at module-load time. +process.env.DEPOSITS_RATE_LIMIT_MAX = '10'; + import request from 'supertest'; import express, { Request, Response } from 'express'; import { authLimiter, depositsLimiter } from '../rateLimiter'; diff --git a/backend/src/__tests__/rateLimiter.test.ts b/backend/src/__tests__/rateLimiter.test.ts index d5d3fa27..ccf2d62e 100644 --- a/backend/src/__tests__/rateLimiter.test.ts +++ b/backend/src/__tests__/rateLimiter.test.ts @@ -291,6 +291,12 @@ describe('depositsLimiter on vault deposit/withdrawal routes', () => { let app: ReturnType; beforeEach(() => { + // This suite tests the real, tight default (10 req/min) — setup.ts + // globally raises DEPOSITS_RATE_LIMIT_MAX for every other integration + // test file, so it must be restored here before the fresh require picks + // it up (this describe block reads process.env fresh on every test via + // jest.resetModules()). + process.env.DEPOSITS_RATE_LIMIT_MAX = '10'; jest.resetModules(); // eslint-disable-next-line @typescript-eslint/no-require-imports const { depositsLimiter } = require('../rateLimiter'); diff --git a/backend/src/__tests__/rbac.test.ts b/backend/src/__tests__/rbac.test.ts index b25742b2..948649d3 100644 --- a/backend/src/__tests__/rbac.test.ts +++ b/backend/src/__tests__/rbac.test.ts @@ -81,7 +81,7 @@ describe('RBAC', () => { const create = await request(app) .post('/admin/webhooks') .set('Authorization', `ApiKey ${adminKey}`) - .send({ url: 'https://example.com/hook', eventTypes: ['transaction.created'] }); + .send({ url: 'https://example.com/hook', eventTypes: ['transaction.deposit.created'] }); expect(create.status).toBe(201); const id = create.body.endpoint.id; @@ -99,7 +99,7 @@ describe('RBAC', () => { const create = await request(app) .post('/admin/webhooks') .set('Authorization', `ApiKey ${adminKey}`) - .send({ url: 'https://example.com/hook2', eventTypes: ['transaction.created'] }); + .send({ url: 'https://example.com/hook2', eventTypes: ['transaction.deposit.created'] }); const id = create.body.endpoint.id; diff --git a/backend/src/__tests__/setup.ts b/backend/src/__tests__/setup.ts index 9a0c6640..37b1a2d5 100644 --- a/backend/src/__tests__/setup.ts +++ b/backend/src/__tests__/setup.ts @@ -23,8 +23,37 @@ process.env.ALLOWLIST_ENABLED = process.env.ALLOWLIST_ENABLED || 'false'; // Disable rate limiting for RBAC/security tests by using very high limits. // The admin limiter defaults to 20 req/min which is too low for comprehensive // endpoint-level security tests that fire 100+ requests in rapid succession. +// Same reasoning extends to every other tier: any integration test file that +// exercises real auth/read/write/summary/deposit routes across several `it` +// blocks can easily exceed the tight production defaults (e.g. auth: 5/min, +// deposits: 10/min) within a single file's run, well before its assertions +// are about rate limiting at all. +// +// rateLimiter.auth_transfer.test.ts intentionally restores the true tight +// DEPOSITS_RATE_LIMIT_MAX default itself (before importing the limiter +// singletons) because it specifically tests deposits/withdrawals throttling +// behavior at that default — everything else is safe to relax globally since +// no other test relies on hitting these defaults via a real, non-harness +// route (rateLimiter.tiers.test.ts's own /auth,/write,/read,/admin routes are +// exempted from config.max entirely in NODE_ENV=test, see +// createInMemoryLimiter's testHarnessDefaults in rateLimiter.ts). process.env.RATE_LIMIT_ADMIN_MAX = process.env.RATE_LIMIT_ADMIN_MAX || '10000'; process.env.RATE_LIMIT_WRITES_MAX = process.env.RATE_LIMIT_WRITES_MAX || '10000'; +process.env.RATE_LIMIT_AUTH_MAX = process.env.RATE_LIMIT_AUTH_MAX || '10000'; +process.env.DEPOSITS_RATE_LIMIT_MAX = process.env.DEPOSITS_RATE_LIMIT_MAX || '10000'; +process.env.RATE_LIMIT_READS_MAX = process.env.RATE_LIMIT_READS_MAX || '10000'; +process.env.SUMMARY_RATE_LIMIT_MAX = process.env.SUMMARY_RATE_LIMIT_MAX || '10000'; +process.env.API_RATE_LIMIT_MAX_REQUESTS = process.env.API_RATE_LIMIT_MAX_REQUESTS || '10000'; + +// Adaptive throttle (src/middleware/adaptiveThrottle.ts) tracks a per-IP +// abuse score across every response in the app (any 4xx adds to it) and, +// once it crosses this threshold, blocks that IP with an escalating delay. +// Integration test files that deliberately exercise several validation-error +// paths (400s, 401s, 403s) accumulate this score exactly like production +// abuse would, then get 429s in later, unrelated test cases in the same +// file. Raise the threshold sky-high so the mechanism stays exercised by its +// own dedicated tests (if any) without tripping incidentally. +process.env.ADAPTIVE_THROTTLE_SCORE_THRESHOLD = process.env.ADAPTIVE_THROTTLE_SCORE_THRESHOLD || '1000000'; // CRITICAL: Patch PrismaClient constructor BEFORE any code tries to instantiate it // This intercepts the instrumentation hooks and prevents the panic @@ -52,14 +81,21 @@ const defaultAdminKey = process.env.ADMIN_API_KEY || 'test-admin-key'; registerApiKey(defaultAdminKey); registerApiKey('super-admin-test-key', { role: 'super-admin' }); -/** Valid 56-character Stellar test wallet (G + 55 base32 chars). */ +/** + * Valid Stellar test wallets — real Ed25519 public keys (correct StrKey + * checksum), not just regex-shaped strings. Endpoints validated via the + * character-class-only regex in @yieldvault/api-schemas tolerated the old + * hand-typed placeholders, but anything validated via the stricter + * StrKey.isValidEd25519PublicKey check (login, nonce, signed actions — + * walletAddressSchema in middleware/validate.ts) rejected them outright. + */ export const VALID_TEST_WALLET = - 'G234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQ'; + 'GBF2VOZSQLF2BZRW6NIDETV2L3I6GHXZUEFSIESELEAGATV2FYTOHOHI'; /** Second valid wallet for multi-wallet test scenarios. */ export const SECOND_TEST_WALLET = - 'G345678ABCDEFGHIJKLMNOPQRSTUVWXYZ345678ABCDEFGHIJKLMNOPQR'; + 'GAB5ZIMWXPZBBTCYQYICI4LJJW2F5PQ3Z3KJJB3SSY66B45KC2KSLIFD'; /** Third valid wallet for multi-wallet test scenarios. */ export const THIRD_TEST_WALLET = - 'G456789ABCDEFGHIJKLMNOPQRSTUVWXYZ456789ABCDEFGHIJKLMNOPQR'; + 'GASEUS2QGJBV5OX2J6AKIORVEA242PQLMRZXCJYN5CZR5G5A65ONOI3N'; diff --git a/backend/src/__tests__/sloMetrics.test.ts b/backend/src/__tests__/sloMetrics.test.ts index 89ea64ee..621143b1 100644 --- a/backend/src/__tests__/sloMetrics.test.ts +++ b/backend/src/__tests__/sloMetrics.test.ts @@ -33,7 +33,7 @@ describe('endpoint SLO Prometheus metrics', () => { latencyMonitoringService.syncSloMetrics(); syncJobGovernanceMetrics(); - let metrics = await register.metrics(); + const metrics = await register.metrics(); expect(metrics).toContain('backend_slo_breach'); expect(metrics).toContain('backend_slo_p95_latency_ms'); expect(metrics).toContain('tier="critical"'); diff --git a/backend/src/__tests__/webhookInputValidation.test.ts b/backend/src/__tests__/webhookInputValidation.test.ts index 15858701..7fd0ac19 100644 --- a/backend/src/__tests__/webhookInputValidation.test.ts +++ b/backend/src/__tests__/webhookInputValidation.test.ts @@ -259,7 +259,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('returns 201 for a minimal valid payload', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 'https://example.com/hook' }); expect(res.status).toBe(201); @@ -271,7 +271,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('returns 201 for a full valid payload', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 'https://example.com/hook', eventTypes: ['transaction.deposit.created'], @@ -288,7 +288,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('returns 400 when url is missing', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ enabled: true }); expect(res.status).toBe(400); @@ -300,7 +300,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('returns 400 when url is not a valid URL', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 'not-a-url' }); expect(res.status).toBe(400); @@ -310,7 +310,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('returns 400 for an unsupported protocol', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 'ftp://example.com/hook' }); expect(res.status).toBe(400); @@ -319,7 +319,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('returns 400 for an unknown event type', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 'https://example.com/hook', eventTypes: ['transaction.unknown'] }); expect(res.status).toBe(400); @@ -329,7 +329,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('returns 400 when eventTypes is an empty array', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 'https://example.com/hook', eventTypes: [] }); expect(res.status).toBe(400); @@ -338,7 +338,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('returns 400 when secret is too short', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 'https://example.com/hook', secret: 'tiny' }); expect(res.status).toBe(400); @@ -348,7 +348,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('returns 400 for unknown extra fields', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 'https://example.com/hook', injected: 'bad' }); expect(res.status).toBe(400); @@ -357,7 +357,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('returns 400 when enabled is not a boolean', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 'https://example.com/hook', enabled: 'yes' }); expect(res.status).toBe(400); @@ -366,7 +366,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('returns 400 when url is a number', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 12345 }); expect(res.status).toBe(400); @@ -375,7 +375,7 @@ describe('POST /admin/webhooks – input validation via HTTP', () => { it('includes structured errors array in validation failure response', async () => { const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ enabled: 'bad-value' }); expect(res.status).toBe(400); @@ -396,7 +396,7 @@ describe('PATCH /admin/webhooks/:id – input validation via HTTP', () => { // Create a fresh endpoint to update in each test const res = await request(app) .post('/admin/webhooks') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 'https://example.com/hook' }); endpointId = res.body.endpoint.id as string; @@ -405,7 +405,7 @@ describe('PATCH /admin/webhooks/:id – input validation via HTTP', () => { it('returns 200 for a valid enabled update', async () => { const res = await request(app) .patch(`/admin/webhooks/${endpointId}`) - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ enabled: false }); expect(res.status).toBe(200); @@ -415,7 +415,7 @@ describe('PATCH /admin/webhooks/:id – input validation via HTTP', () => { it('returns 200 for a valid eventTypes update', async () => { const res = await request(app) .patch(`/admin/webhooks/${endpointId}`) - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ eventTypes: ['transaction.deposit.created'] }); expect(res.status).toBe(200); @@ -425,7 +425,7 @@ describe('PATCH /admin/webhooks/:id – input validation via HTTP', () => { it('returns 400 for an empty body', async () => { const res = await request(app) .patch(`/admin/webhooks/${endpointId}`) - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({}); expect(res.status).toBe(400); @@ -435,7 +435,7 @@ describe('PATCH /admin/webhooks/:id – input validation via HTTP', () => { it('returns 400 for an invalid url in update', async () => { const res = await request(app) .patch(`/admin/webhooks/${endpointId}`) - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ url: 'javascript://xss' }); expect(res.status).toBe(400); @@ -444,7 +444,7 @@ describe('PATCH /admin/webhooks/:id – input validation via HTTP', () => { it('returns 400 for an empty eventTypes array in update', async () => { const res = await request(app) .patch(`/admin/webhooks/${endpointId}`) - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ eventTypes: [] }); expect(res.status).toBe(400); @@ -453,7 +453,7 @@ describe('PATCH /admin/webhooks/:id – input validation via HTTP', () => { it('returns 400 for an unknown event type in update', async () => { const res = await request(app) .patch(`/admin/webhooks/${endpointId}`) - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ eventTypes: ['transaction.bogus'] }); expect(res.status).toBe(400); @@ -462,7 +462,7 @@ describe('PATCH /admin/webhooks/:id – input validation via HTTP', () => { it('returns 400 for a secret shorter than 8 chars', async () => { const res = await request(app) .patch(`/admin/webhooks/${endpointId}`) - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ secret: 'abc' }); expect(res.status).toBe(400); @@ -471,7 +471,7 @@ describe('PATCH /admin/webhooks/:id – input validation via HTTP', () => { it('returns 400 for unknown extra fields', async () => { const res = await request(app) .patch(`/admin/webhooks/${endpointId}`) - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ enabled: true, injected: 'bad' }); expect(res.status).toBe(400); @@ -480,7 +480,7 @@ describe('PATCH /admin/webhooks/:id – input validation via HTTP', () => { it('returns 404 for a non-existent endpoint id', async () => { const res = await request(app) .patch('/admin/webhooks/wh_nonexistent') - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ enabled: false }); expect(res.status).toBe(404); @@ -489,7 +489,7 @@ describe('PATCH /admin/webhooks/:id – input validation via HTTP', () => { it('includes structured errors in validation failure response', async () => { const res = await request(app) .patch(`/admin/webhooks/${endpointId}`) - .set('x-api-key', ADMIN_KEY) + .set('Authorization', `ApiKey ${ADMIN_KEY}`) .send({ enabled: 'not-a-bool' }); expect(res.status).toBe(400); diff --git a/backend/src/__tests__/withdrawalRecoveryEndpoint.test.ts b/backend/src/__tests__/withdrawalRecoveryEndpoint.test.ts index 3ad8386f..fd878796 100644 --- a/backend/src/__tests__/withdrawalRecoveryEndpoint.test.ts +++ b/backend/src/__tests__/withdrawalRecoveryEndpoint.test.ts @@ -24,6 +24,16 @@ jest.mock('../prismaClient', () => ({ disconnectPrismaClient: jest.fn().mockResolvedValue(undefined), })); +// Deposits call referralService.recordDeposit(), which resolves the wallet's +// canonical identity and opens its own Prisma transaction for referral +// bookkeeping — neither of which this file's minimal Prisma mock supports, +// and neither of which this suite (withdrawal saga recovery) is testing. +jest.mock('../referralService', () => ({ + referralService: { + recordDeposit: jest.fn().mockResolvedValue(undefined), + }, +})); + // The signed-action middleware pulls in the wallet nonce service, which is not // exercised here (signature enforcement is off in tests). jest.mock('../walletNonce', () => ({ diff --git a/backend/src/apiContractSnapshots.ts b/backend/src/apiContractSnapshots.ts index 8918cc5e..e818138c 100644 --- a/backend/src/apiContractSnapshots.ts +++ b/backend/src/apiContractSnapshots.ts @@ -29,6 +29,7 @@ export const HealthResponseSchema = z timestamp: z.string(), uptime: z.number(), environment: z.string(), + lastIndexedLedger: z.number(), checks: z.object({ api: HealthCheckValueSchema, cache: HealthCheckValueSchema, @@ -61,8 +62,12 @@ export const ReadyResponseSchema = z export const VaultSummaryResponseSchema = z .object({ - totalAssets: z.number(), - totalShares: z.number(), + // Money fields are strings throughout this API (Decimal-backed, to avoid + // floating-point precision loss) — see buildVaultSummaryResponseFromDb in + // index.ts, which returns totalAssets/totalShares/sharePrice as strings. + totalAssets: z.string(), + totalShares: z.string(), + sharePrice: z.string(), apy: z.number(), timestamp: z.string(), }) diff --git a/backend/src/apySnapshot.ts b/backend/src/apySnapshot.ts index ad7a5e31..87f972c4 100644 --- a/backend/src/apySnapshot.ts +++ b/backend/src/apySnapshot.ts @@ -77,7 +77,7 @@ function datePlusDays(date: string, days: number): string { */ async function fetchCurrentApy(): Promise { // Mock: query the DB / RPC for the live APY gauge - await db.query('SELECT apy FROM vault_metrics ORDER BY recorded_at DESC LIMIT 1'); + await db.query('SELECT apy FROM vault_metrics_snapshots ORDER BY recorded_at DESC LIMIT 1'); // Return a realistic mock value between 6% and 12% const base = 8.5; const jitter = (Math.random() - 0.5) * 0.4; diff --git a/backend/src/database.ts b/backend/src/database.ts index 23985de3..75057250 100644 --- a/backend/src/database.ts +++ b/backend/src/database.ts @@ -53,6 +53,28 @@ export class PostgresDatabasePool implements IDatabasePool { } } +/** + * No-op pool used in place of a real Postgres connection when running tests + * without a provisioned database (DATABASE_URL unset). Queries resolve to an + * empty result set instead of attempting — and failing — a real network + * connection, and health checks report healthy so `/health`/`/ready` reflect + * the actual state of the test environment rather than a database no test + * suite ever provisions. + */ +export class NoopDatabasePool implements IDatabasePool { + constructor(private readonly name: string) {} + + async query(_text: string, _params?: any[]): Promise<{ rows: T[] }> { + return { rows: [] }; + } + + async end(): Promise {} + + async isHealthy(): Promise { + return true; + } +} + /** * DatabaseManager handles routing queries between a primary write database * and a read-only replica, with automatic failover to primary for reads. @@ -66,6 +88,15 @@ export class DatabaseManager { if (primaryPool) { this.primaryPool = primaryPool; this.replicaPool = replicaPool || primaryPool; + } else if (process.env.NODE_ENV === 'test' && !process.env.DATABASE_URL) { + // No test suite provisions a real Postgres instance, and unlike Prisma + // (which falls back to its own sqlite dev.db) this raw pool had no + // test-mode fallback at all — every query attempted a real connection + // to the hardcoded dev default below, which no test environment + // actually runs. That made /health and anything touching `db` fail + // for a reason entirely unrelated to what the test was exercising. + this.primaryPool = new NoopDatabasePool('primary'); + this.replicaPool = this.primaryPool; } else { const primaryUrl = requireDatabaseUrl(); this.primaryPool = new PostgresDatabasePool(primaryUrl, 'primary'); diff --git a/backend/src/eventPollingService.ts b/backend/src/eventPollingService.ts index 8cd6593e..b52017a8 100644 --- a/backend/src/eventPollingService.ts +++ b/backend/src/eventPollingService.ts @@ -48,8 +48,17 @@ export class EventPollingService { // Acquire leader lock before starting polling await this.acquireLeaderLock(); - // Replay missed events on startup - let errors propagate - await this.replayMissedEvents(); + // Replay missed events on startup. A Soroban RPC outage here must not + // prevent continuous polling from ever starting — the next successful + // poll will re-derive the same missed range from the persisted cursor, + // so a failed replay is a delayed catch-up rather than lost data. + try { + await this.replayMissedEvents(); + } catch (error) { + logger.log('warn', 'Event replay on startup failed — external dependency outage suspected, continuing in degraded mode', { + error: error instanceof Error ? error.message : 'Unknown error', + }); + } // Start continuous polling only if we are the leader if (this.isLeader) { diff --git a/backend/src/index.ts b/backend/src/index.ts index cbb48f4d..70e2c0c9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -240,15 +240,6 @@ void walletAliasMappingService.loadFromDatabase().catch((error) => { // Health check cache to track dependency status const cache = new NodeCache({ stdTTL: 30 }); -function buildVaultSummaryResponse() { - return { - totalAssets: 0, - totalShares: 0, - apy: 0, - timestamp: new Date().toISOString(), - }; -} - /** * Reads the vault summary from the VaultState table and the most recent * SharePriceSnapshot for the share price. Falls back to zeroed values when @@ -410,7 +401,10 @@ async function buildImpersonatedVaultState(wallet: string) { const normalizedWallet = normalizeWalletAddress(wallet); return { walletAddress: normalizedWallet, - summary: buildVaultSummaryResponse(), + // Impersonation must show an admin exactly what the wallet's own + // dashboard would show, so read the same DB-backed summary + // GET /api/v1/vault/summary serves. + summary: await buildVaultSummaryResponseFromDb(), transactions: await buildWalletTransactionsSnapshot(normalizedWallet), portfolioHoldings: buildPortfolioHoldingsResponse({ walletAddress: normalizedWallet }), vaultHistory: buildVaultHistoryResponse({}), @@ -5133,9 +5127,21 @@ app.post('/admin/withdrawals/recovery/sweep', validateApiKey, async (req: Reques // suites drive the sweeper explicitly. if (process.env.NODE_ENV !== 'test') { withdrawalRecoveryCoordinator.startSweeper(); - + // Initialize job governance from persisted dead-letter records void initializeJobGovernance(); } +// Catch-all 404 handler. Must be the last middleware registered so it only +// fires for requests no route above matched, instead of Express's default +// plain-text/HTML 404 page. +app.use((req: Request, res: Response) => { + res.status(404).json({ + error: 'Not Found', + status: 404, + path: req.originalUrl, + message: `Cannot ${req.method} ${req.originalUrl}`, + }); +}); + export default app; diff --git a/backend/src/listEndpoints.ts b/backend/src/listEndpoints.ts index 406083bb..257007e2 100644 --- a/backend/src/listEndpoints.ts +++ b/backend/src/listEndpoints.ts @@ -158,7 +158,10 @@ export interface TransactionExportArtifact { // ─── Mock Data ────────────────────────────────────────────────────────────── -const MOCK_WALLET_ADDRESS = 'G234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQ'; +// Kept in sync with VALID_TEST_WALLET in src/__tests__/setup.ts — both must be +// a real, checksum-valid Ed25519 public key (not just regex-shaped) since +// some endpoints validate wallet addresses via StrKey.isValidEd25519PublicKey. +const MOCK_WALLET_ADDRESS = 'GBF2VOZSQLF2BZRW6NIDETV2L3I6GHXZUEFSIESELEAGATV2FYTOHOHI'; const MOCK_TRANSACTIONS: Transaction[] = Array.from({ length: 100 }, (_, i) => ({ id: `tx-${i + 1}`, diff --git a/backend/src/optimisticConcurrency.ts b/backend/src/optimisticConcurrency.ts index b2c89582..1187bcc7 100644 --- a/backend/src/optimisticConcurrency.ts +++ b/backend/src/optimisticConcurrency.ts @@ -40,7 +40,7 @@ export async function executeWithOptimisticConcurrency( const maxDelayMs = options?.maxDelayMs ?? DEFAULT_OPTIONS.maxDelayMs; let attempt = 0; - while (true) { + for (;;) { attempt++; try { return await operation(attempt); diff --git a/backend/src/prisma.ts b/backend/src/prisma.ts index a66c948d..3a353dc1 100644 --- a/backend/src/prisma.ts +++ b/backend/src/prisma.ts @@ -24,23 +24,21 @@ function buildDatasourceUrl(): string | undefined { return rawUrl; } + // schema.prisma's datasource provider is "sqlite" — its generated client + // can only ever accept file: URLs, regardless of what DATABASE_URL holds. + // DATABASE_URL pointing at Postgres/MySQL is for the separate raw `pg` + // pool in database.ts (its own migrations, its own tables); overriding + // Prisma's datasource with that URL here would fail schema validation + // outright, so fall back to the schema-declared sqlite file instead of + // crashing every Prisma-backed query. try { const url = new URL(rawUrl); - if (url.protocol.startsWith('postgres')) { - url.searchParams.set('connection_limit', String(POOL_MAX)); - url.searchParams.set('pool_timeout', String(Math.round(POOL_TIMEOUT_MS / 1000))); - return url.toString(); + if (url.protocol.startsWith('postgres') || url.protocol.startsWith('mysql')) { + return undefined; } - - if (url.protocol.startsWith('mysql')) { - url.searchParams.set('connection_limit', String(POOL_MAX)); - url.searchParams.set('pool_timeout', String(POOL_TIMEOUT_MS)); - return url.toString(); - } - return rawUrl; } catch { - return rawUrl; + return undefined; } } diff --git a/backend/src/transactionEndpoints.ts b/backend/src/transactionEndpoints.ts index eaf24235..ace2d71f 100644 --- a/backend/src/transactionEndpoints.ts +++ b/backend/src/transactionEndpoints.ts @@ -63,7 +63,7 @@ router.get('/', if (!walletAddress) { try { - const response = buildTransactionsResponse({ + const response = await buildTransactionsResponse({ limit: typeof req.query.limit === 'string' ? parseInt(req.query.limit, 10) : undefined, cursor: typeof req.query.cursor === 'string' ? req.query.cursor : undefined, page: typeof req.query.page === 'string' ? parseInt(req.query.page, 10) : undefined, diff --git a/backend/src/walletAliasService.ts b/backend/src/walletAliasService.ts index 1ea9503c..8c046d80 100644 --- a/backend/src/walletAliasService.ts +++ b/backend/src/walletAliasService.ts @@ -354,7 +354,7 @@ export class WalletAliasMappingService { } private async createCanonicalId(tx: PrismaTransaction): Promise { - while (true) { + for (;;) { this.canonicalIdCounter += 1; const id = `wallet-alias:${this.canonicalIdCounter}`; const existing = await tx.walletCanonicalIdentity.findUnique({ where: { id } }); diff --git a/backend/src/writeAheadAuditLog.ts b/backend/src/writeAheadAuditLog.ts index 93e0487c..71e61b70 100644 --- a/backend/src/writeAheadAuditLog.ts +++ b/backend/src/writeAheadAuditLog.ts @@ -46,7 +46,7 @@ const DEFAULT_STALE_PENDING_TTL_MS = 15 * 60 * 1000; function getStalePendingTtlMs(): number { const parsed = parseInt(process.env.WAL_STALE_PENDING_TTL_MS || '', 10); - return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_STALE_PENDING_TTL_MS; + return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_STALE_PENDING_TTL_MS; } /**