diff --git a/.github/workflows/benchmark-api-smoke.yml b/.github/workflows/benchmark-api-smoke.yml new file mode 100644 index 000000000..87d946185 --- /dev/null +++ b/.github/workflows/benchmark-api-smoke.yml @@ -0,0 +1,41 @@ +name: API benchmark smoke + +on: + pull_request: + paths: + - "apps/api/**" + - "benchmarks/**" + - "package.json" + - "package-lock.json" + - ".github/workflows/benchmark-api-smoke.yml" + push: + branches: + - main + paths: + - "apps/api/**" + - "benchmarks/**" + - "package.json" + - "package-lock.json" + +jobs: + benchmark-smoke: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run API test suite + run: npm run test + + - name: Run smoke benchmark gate + run: npm run benchmark:smoke diff --git a/.gitignore b/.gitignore index 1e3ce10dd..9bdf6b263 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ node_modules dist .env .env.* +!benchmarks/.env.benchmark.example coverage *.log diff --git a/README.md b/README.md index e3a09b139..408b8b6c3 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Backend architecture follows: ```bash npm install npm run test +npm run benchmark ``` ## AI Agent Contribution Instruction @@ -86,3 +87,14 @@ Prisma schema is available in `packages/db/prisma/schema.prisma` with models for ## Environment Variables Each app/package expects its own `.env` values for DB, auth, and integrations. + +## Benchmarking + +The repository includes a committed benchmark suite under [`benchmarks`](./benchmarks) +that exercises every `/api/` endpoint with schema-shaped payloads, records p50/p95/p99 +latency, peak and sustained request throughput, error rate, and TTFB, and writes JSON +plus markdown reports to [`benchmarks/results`](./benchmarks/results). + +- Full suite: `npm run benchmark` +- CI smoke gate: `npm run benchmark:smoke` +- Configuration template: `benchmarks/.env.benchmark.example` diff --git a/apps/api/package.json b/apps/api/package.json index 25fa0e90e..f558b71ca 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "node src/server.js", "start": "node src/server.js", - "test": "node --test src/tests" + "test": "node --test \"src/tests/*.test.js\"" }, "dependencies": { "cors": "^2.8.5", diff --git a/apps/api/src/config/env.js b/apps/api/src/config/env.js index 9eaf7f098..7faaa389b 100644 --- a/apps/api/src/config/env.js +++ b/apps/api/src/config/env.js @@ -1,4 +1,5 @@ export const env = { + benchmarkMode: process.env.BENCHMARK_MODE === "1", nodeEnv: process.env.NODE_ENV ?? "development", port: Number(process.env.PORT ?? 4000), jwtSecret: process.env.JWT_SECRET ?? "development-secret", diff --git a/apps/api/src/middleware/rateLimit.js b/apps/api/src/middleware/rateLimit.js index 6b6280103..eb81adcef 100644 --- a/apps/api/src/middleware/rateLimit.js +++ b/apps/api/src/middleware/rateLimit.js @@ -1,8 +1,11 @@ import rateLimit from "express-rate-limit"; +import { env } from "../config/env.js"; -export const apiLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, - limit: 200, - standardHeaders: "draft-7", - legacyHeaders: false -}); +export const apiLimiter = env.benchmarkMode + ? (req, res, next) => next() + : rateLimit({ + windowMs: 15 * 60 * 1000, + limit: 200, + standardHeaders: "draft-7", + legacyHeaders: false + }); diff --git a/benchmarks/.env.benchmark.example b/benchmarks/.env.benchmark.example new file mode 100644 index 000000000..fe2d1ce6c --- /dev/null +++ b/benchmarks/.env.benchmark.example @@ -0,0 +1,10 @@ +BENCHMARK_TARGET=local +BENCHMARK_BASE_URL= +BENCHMARK_PORT=4010 +BENCHMARK_PROFILE=full +BENCHMARK_CONCURRENCY= +BENCHMARK_AMOUNT= +BENCHMARK_TTFB_SAMPLES=5 +BENCHMARK_ADMIN_TOKEN= +BENCHMARK_MODE=1 +JWT_SECRET=benchmark-secret diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 000000000..186ab42d3 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,21 @@ +# API Benchmarks + +The benchmark suite covers every `/api/` endpoint in the repository and writes +JSON plus markdown summaries to [`benchmarks/results`](./results). + +## Setup + +1. Copy [`benchmarks/.env.benchmark.example`](./.env.benchmark.example) to `benchmarks/.env.benchmark`. +2. Adjust `BENCHMARK_TARGET` and `BENCHMARK_BASE_URL` if you want to hit a staging host instead of the local API. +3. Run `npm run benchmark` for the full suite or `npm run benchmark:smoke` for the low-concurrency CI profile. + +When `BENCHMARK_TARGET=local`, the runner starts the Express app in benchmark mode, +uses a dedicated JWT benchmark token for `/api/admin/metrics`, and bypasses API +rate limiting so latency numbers are not polluted by 429 responses. + +If you benchmark a remote or staging deployment, set `BENCHMARK_ADMIN_TOKEN` in +`benchmarks/.env.benchmark` so the protected admin endpoint is exercised with a +real dedicated benchmark token. + +For bounty submissions, paste the generated `benchmarks/results/latest-full.md` +summary into the PR description. diff --git a/benchmarks/fixtures/upload-sample.txt b/benchmarks/fixtures/upload-sample.txt new file mode 100644 index 000000000..bccfccbfa --- /dev/null +++ b/benchmarks/fixtures/upload-sample.txt @@ -0,0 +1,4 @@ +FreelanceFlow benchmark upload fixture + +This text fixture is used to exercise the multipart upload endpoint during +local and CI smoke benchmark runs. diff --git a/benchmarks/results/.gitkeep b/benchmarks/results/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/benchmarks/results/.gitkeep @@ -0,0 +1 @@ + diff --git a/benchmarks/results/latest-full.json b/benchmarks/results/latest-full.json new file mode 100644 index 000000000..950b22990 --- /dev/null +++ b/benchmarks/results/latest-full.json @@ -0,0 +1,619 @@ +{ + "generatedAt": "2026-05-22T21:41:57.849Z", + "profile": "full", + "baseUrl": "http://127.0.0.1:4010", + "environment": { + "profile": "full", + "baseUrl": "http://127.0.0.1:4010", + "target": "local", + "hardware": { + "cpuModel": "13th Gen Intel(R) Core(TM) i5-13600K", + "coreCount": 20, + "ramTotalGb": 31.75, + "ramFreeGb": 13.85, + "storageType": "unknown", + "networkInterface": "loopback", + "machineType": "local workstation" + }, + "runtime": { + "nodeVersion": "v22.18.0", + "os": "win32 10.0.26200", + "resourceLimits": "none declared", + "backgroundProcesses": "unknown" + }, + "aiAgent": { + "toolName": "Codex", + "model": "GPT-5", + "provider": "OpenAI", + "orchestration": "Codex desktop", + "executionMode": "fully autonomous", + "shellAccess": true, + "internetAccess": true, + "benchmarkExecutedByAgent": true, + "knownConstraints": "No GitHub CLI in PATH; some authenticated payout pages are browser-policy limited." + } + }, + "results": [ + { + "name": "auth-register", + "routePath": "/api/auth/register", + "path": "/api/auth/register", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 90, + "requestsSent": 80, + "statusCodes": { + "201": 80 + }, + "latencyMs": { + "p50": 4.33, + "p95": 21.07, + "p99": 21.39, + "average": 5.53 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 3.44, + "p95Ms": 9.79, + "maxMs": 9.79 + } + }, + { + "name": "auth-login", + "routePath": "/api/auth/login", + "path": "/api/auth/login", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 71, + "requestsSent": 80, + "statusCodes": { + "200": 80 + }, + "latencyMs": { + "p50": 4.17, + "p95": 6, + "p99": 6.31, + "average": 4 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.54, + "p95Ms": 2.01, + "maxMs": 2.01 + } + }, + { + "name": "auth-oauth-callback", + "routePath": "/api/auth/oauth/github/callback", + "path": "/api/auth/oauth/github/callback", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 80, + "statusCodes": { + "200": 80 + }, + "latencyMs": { + "p50": 1.53, + "p95": 2.99, + "p99": 3.11, + "average": 1.25 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 0.96, + "p95Ms": 1.82, + "maxMs": 1.82 + } + }, + { + "name": "auth-refresh", + "routePath": "/api/auth/refresh", + "path": "/api/auth/refresh", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 2, + "requestsSent": 80, + "statusCodes": { + "200": 80 + }, + "latencyMs": { + "p50": 6.94, + "p95": 10.61, + "p99": 11.12, + "average": 6.69 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 2.52, + "p95Ms": 5.91, + "maxMs": 5.91 + } + }, + { + "name": "users-list", + "routePath": "/api/users", + "path": "/api/users", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 80, + "statusCodes": { + "200": 80 + }, + "latencyMs": { + "p50": 2.19, + "p95": 8.32, + "p99": 8.4, + "average": 2.6 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.05, + "p95Ms": 1.78, + "maxMs": 1.78 + } + }, + { + "name": "users-create", + "routePath": "/api/users", + "path": "/api/users", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 352, + "requestsSent": 80, + "statusCodes": { + "201": 80 + }, + "latencyMs": { + "p50": 3.05, + "p95": 6.06, + "p99": 6.16, + "average": 2.73 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.6, + "p95Ms": 2.9, + "maxMs": 2.9 + } + }, + { + "name": "jobs-list", + "routePath": "/api/jobs", + "path": "/api/jobs", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 80, + "statusCodes": { + "200": 80 + }, + "latencyMs": { + "p50": 2.55, + "p95": 8.22, + "p99": 8.38, + "average": 2.53 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 0.77, + "p95Ms": 1.11, + "maxMs": 1.11 + } + }, + { + "name": "jobs-create", + "routePath": "/api/jobs", + "path": "/api/jobs", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 673, + "requestsSent": 80, + "statusCodes": { + "201": 80 + }, + "latencyMs": { + "p50": 3.97, + "p95": 10.13, + "p99": 10.45, + "average": 4.18 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.32, + "p95Ms": 1.79, + "maxMs": 1.79 + } + }, + { + "name": "proposals-list", + "routePath": "/api/proposals", + "path": "/api/proposals", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 80, + "statusCodes": { + "200": 80 + }, + "latencyMs": { + "p50": 2.71, + "p95": 8.3, + "p99": 8.98, + "average": 3.19 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 0.99, + "p95Ms": 1.81, + "maxMs": 1.81 + } + }, + { + "name": "proposals-create", + "routePath": "/api/proposals", + "path": "/api/proposals", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 552, + "requestsSent": 80, + "statusCodes": { + "201": 80 + }, + "latencyMs": { + "p50": 2.73, + "p95": 9.28, + "p99": 9.36, + "average": 3.2 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.2, + "p95Ms": 1.5, + "maxMs": 1.5 + } + }, + { + "name": "payments-create", + "routePath": "/api/payments", + "path": "/api/payments", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 34, + "requestsSent": 80, + "statusCodes": { + "201": 80 + }, + "latencyMs": { + "p50": 3.09, + "p95": 6.36, + "p99": 6.46, + "average": 2.95 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.07, + "p95Ms": 1.4, + "maxMs": 1.4 + } + }, + { + "name": "reviews-list", + "routePath": "/api/reviews", + "path": "/api/reviews", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 80, + "statusCodes": { + "200": 80 + }, + "latencyMs": { + "p50": 2.13, + "p95": 5.08, + "p99": 5.18, + "average": 1.98 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 0.75, + "p95Ms": 1.05, + "maxMs": 1.05 + } + }, + { + "name": "reviews-create", + "routePath": "/api/reviews", + "path": "/api/reviews", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 376, + "requestsSent": 80, + "statusCodes": { + "201": 80 + }, + "latencyMs": { + "p50": 2.89, + "p95": 5.52, + "p99": 5.56, + "average": 2.78 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.57, + "p95Ms": 4.09, + "maxMs": 4.09 + } + }, + { + "name": "messages-list", + "routePath": "/api/messages", + "path": "/api/messages", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 80, + "statusCodes": { + "200": 80 + }, + "latencyMs": { + "p50": 2.18, + "p95": 6, + "p99": 6.02, + "average": 1.93 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.06, + "p95Ms": 1.73, + "maxMs": 1.73 + } + }, + { + "name": "messages-create", + "routePath": "/api/messages", + "path": "/api/messages", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 357, + "requestsSent": 80, + "statusCodes": { + "201": 80 + }, + "latencyMs": { + "p50": 3.65, + "p95": 12.46, + "p99": 12.5, + "average": 3.74 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 2.82, + "p95Ms": 9.8, + "maxMs": 9.8 + } + }, + { + "name": "notifications-list", + "routePath": "/api/notifications", + "path": "/api/notifications", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 80, + "statusCodes": { + "200": 80 + }, + "latencyMs": { + "p50": 2.68, + "p95": 6.87, + "p99": 7.69, + "average": 2.63 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.07, + "p95Ms": 2.22, + "maxMs": 2.22 + } + }, + { + "name": "notifications-create", + "routePath": "/api/notifications", + "path": "/api/notifications", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 311, + "requestsSent": 80, + "statusCodes": { + "201": 80 + }, + "latencyMs": { + "p50": 3.01, + "p95": 6.78, + "p99": 7.74, + "average": 3.12 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.18, + "p95Ms": 1.69, + "maxMs": 1.69 + } + }, + { + "name": "uploads-create", + "routePath": "/api/uploads", + "path": "/api/uploads", + "method": "POST", + "authProtected": false, + "payloadType": "multipart", + "payloadBytes": 150, + "requestsSent": 80, + "statusCodes": { + "201": 80 + }, + "latencyMs": { + "p50": 4.77, + "p95": 13.26, + "p99": 13.39, + "average": 5.15 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 2.25, + "p95Ms": 3.21, + "maxMs": 3.21 + } + }, + { + "name": "search-query", + "routePath": "/api/search", + "path": "/api/search?q=marketplace+payout+benchmark+plan", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 80, + "statusCodes": { + "200": 80 + }, + "latencyMs": { + "p50": 4.7, + "p95": 7.48, + "p99": 8.64, + "average": 4.57 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.03, + "p95Ms": 1.7, + "maxMs": 1.7 + } + }, + { + "name": "admin-metrics", + "routePath": "/api/admin/metrics", + "path": "/api/admin/metrics", + "method": "GET", + "authProtected": true, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 80, + "statusCodes": { + "200": 80 + }, + "latencyMs": { + "p50": 6.42, + "p95": 14.49, + "p99": 14.5, + "average": 7.13 + }, + "throughput": { + "sustainedRps": 80, + "peakRps": 80 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.81, + "p95Ms": 2.66, + "maxMs": 2.66 + } + } + ], + "thresholdFailures": [] +} diff --git a/benchmarks/results/latest-full.md b/benchmarks/results/latest-full.md new file mode 100644 index 000000000..0e6e05be7 --- /dev/null +++ b/benchmarks/results/latest-full.md @@ -0,0 +1,57 @@ +# API Benchmark Summary + +- Generated at: 2026-05-22T21:41:57.849Z +- Profile: full +- Base URL: http://127.0.0.1:4010 +- Endpoints covered: 20 + +## Benchmark Environment + +**Hardware** +- CPU model & core count: 13th Gen Intel(R) Core(TM) i5-13600K (20 cores) +- RAM (total & available during benchmark): 31.75 GB total / 13.85 GB free +- Storage type (SSD / NVMe / HDD): unknown +- Network interface (Ethernet / WiFi / loopback): loopback +- Machine type (local workstation / cloud VM / CI runner - include instance type if cloud): local workstation +- OS & version: win32 10.0.26200 + +**Runtime** +- Node.js version (or relevant runtime): v22.18.0 +- Any resource limits applied (Docker memory cap, cgroup limits, etc.): none declared +- Other significant processes running during benchmark (yes / no - if yes, describe): unknown + +**If submitted by or with an AI agent** +- Agent or tool name: Codex +- Underlying model and version: GPT-5 +- Inference provider: OpenAI +- Orchestration framework if any: Codex desktop +- Execution mode: fully autonomous +- Did the agent have shell/tool access during execution: yes +- Did the agent have internet access during execution: yes +- Were benchmark commands run by the agent directly or handed off to the human to run: agent directly +- Any known agent constraints or sandboxing that may have affected execution: No GitHub CLI in PATH; some authenticated payout pages are browser-policy limited. + +## Endpoint Metrics + +| Endpoint | Method | Auth | Payload bytes | p50 ms | p95 ms | p99 ms | Avg ms | Sustained req/s | Peak req/s | Error % | Avg TTFB ms | p95 TTFB ms | +| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| /api/auth/register | POST | public | 90 | 4.33 | 21.07 | 21.39 | 5.53 | 80 | 80 | 0 | 3.44 | 9.79 | +| /api/auth/login | POST | public | 71 | 4.17 | 6 | 6.31 | 4 | 80 | 80 | 0 | 1.54 | 2.01 | +| /api/auth/oauth/github/callback | GET | public | 0 | 1.53 | 2.99 | 3.11 | 1.25 | 80 | 80 | 0 | 0.96 | 1.82 | +| /api/auth/refresh | POST | public | 2 | 6.94 | 10.61 | 11.12 | 6.69 | 80 | 80 | 0 | 2.52 | 5.91 | +| /api/users | GET | public | 0 | 2.19 | 8.32 | 8.4 | 2.6 | 80 | 80 | 0 | 1.05 | 1.78 | +| /api/users | POST | public | 352 | 3.05 | 6.06 | 6.16 | 2.73 | 80 | 80 | 0 | 1.6 | 2.9 | +| /api/jobs | GET | public | 0 | 2.55 | 8.22 | 8.38 | 2.53 | 80 | 80 | 0 | 0.77 | 1.11 | +| /api/jobs | POST | public | 673 | 3.97 | 10.13 | 10.45 | 4.18 | 80 | 80 | 0 | 1.32 | 1.79 | +| /api/proposals | GET | public | 0 | 2.71 | 8.3 | 8.98 | 3.19 | 80 | 80 | 0 | 0.99 | 1.81 | +| /api/proposals | POST | public | 552 | 2.73 | 9.28 | 9.36 | 3.2 | 80 | 80 | 0 | 1.2 | 1.5 | +| /api/payments | POST | public | 34 | 3.09 | 6.36 | 6.46 | 2.95 | 80 | 80 | 0 | 1.07 | 1.4 | +| /api/reviews | GET | public | 0 | 2.13 | 5.08 | 5.18 | 1.98 | 80 | 80 | 0 | 0.75 | 1.05 | +| /api/reviews | POST | public | 376 | 2.89 | 5.52 | 5.56 | 2.78 | 80 | 80 | 0 | 1.57 | 4.09 | +| /api/messages | GET | public | 0 | 2.18 | 6 | 6.02 | 1.93 | 80 | 80 | 0 | 1.06 | 1.73 | +| /api/messages | POST | public | 357 | 3.65 | 12.46 | 12.5 | 3.74 | 80 | 80 | 0 | 2.82 | 9.8 | +| /api/notifications | GET | public | 0 | 2.68 | 6.87 | 7.69 | 2.63 | 80 | 80 | 0 | 1.07 | 2.22 | +| /api/notifications | POST | public | 311 | 3.01 | 6.78 | 7.74 | 3.12 | 80 | 80 | 0 | 1.18 | 1.69 | +| /api/uploads | POST | public | 150 | 4.77 | 13.26 | 13.39 | 5.15 | 80 | 80 | 0 | 2.25 | 3.21 | +| /api/search?q=marketplace+payout+benchmark+plan | GET | public | 0 | 4.7 | 7.48 | 8.64 | 4.57 | 80 | 80 | 0 | 1.03 | 1.7 | +| /api/admin/metrics | GET | benchmark token | 0 | 6.42 | 14.49 | 14.5 | 7.13 | 80 | 80 | 0 | 1.81 | 2.66 | diff --git a/benchmarks/results/latest-smoke.json b/benchmarks/results/latest-smoke.json new file mode 100644 index 000000000..9c3b451a0 --- /dev/null +++ b/benchmarks/results/latest-smoke.json @@ -0,0 +1,619 @@ +{ + "generatedAt": "2026-05-22T21:41:18.494Z", + "profile": "smoke", + "baseUrl": "http://127.0.0.1:4010", + "environment": { + "profile": "smoke", + "baseUrl": "http://127.0.0.1:4010", + "target": "local", + "hardware": { + "cpuModel": "13th Gen Intel(R) Core(TM) i5-13600K", + "coreCount": 20, + "ramTotalGb": 31.75, + "ramFreeGb": 13.79, + "storageType": "unknown", + "networkInterface": "loopback", + "machineType": "local workstation" + }, + "runtime": { + "nodeVersion": "v22.18.0", + "os": "win32 10.0.26200", + "resourceLimits": "none declared", + "backgroundProcesses": "unknown" + }, + "aiAgent": { + "toolName": "Codex", + "model": "GPT-5", + "provider": "OpenAI", + "orchestration": "Codex desktop", + "executionMode": "fully autonomous", + "shellAccess": true, + "internetAccess": true, + "benchmarkExecutedByAgent": true, + "knownConstraints": "No GitHub CLI in PATH; some authenticated payout pages are browser-policy limited." + } + }, + "results": [ + { + "name": "auth-register", + "routePath": "/api/auth/register", + "path": "/api/auth/register", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 90, + "requestsSent": 24, + "statusCodes": { + "201": 24 + }, + "latencyMs": { + "p50": 1.64, + "p95": 16.71, + "p99": 16.82, + "average": 2.59 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 3.57, + "p95Ms": 10.04, + "maxMs": 10.04 + } + }, + { + "name": "auth-login", + "routePath": "/api/auth/login", + "path": "/api/auth/login", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 71, + "requestsSent": 24, + "statusCodes": { + "200": 24 + }, + "latencyMs": { + "p50": 1.45, + "p95": 3.06, + "p99": 3.1, + "average": 1.17 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.92, + "p95Ms": 2.87, + "maxMs": 2.87 + } + }, + { + "name": "auth-oauth-callback", + "routePath": "/api/auth/oauth/github/callback", + "path": "/api/auth/oauth/github/callback", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 24, + "statusCodes": { + "200": 24 + }, + "latencyMs": { + "p50": 0.54, + "p95": 1.82, + "p99": 1.92, + "average": 0.09 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 2.36, + "p95Ms": 5.6, + "maxMs": 5.6 + } + }, + { + "name": "auth-refresh", + "routePath": "/api/auth/refresh", + "path": "/api/auth/refresh", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 2, + "requestsSent": 24, + "statusCodes": { + "200": 24 + }, + "latencyMs": { + "p50": 2.26, + "p95": 4.56, + "p99": 4.61, + "average": 2.17 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.97, + "p95Ms": 3.21, + "maxMs": 3.21 + } + }, + { + "name": "users-list", + "routePath": "/api/users", + "path": "/api/users", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 24, + "statusCodes": { + "200": 24 + }, + "latencyMs": { + "p50": 0.95, + "p95": 2.48, + "p99": 2.69, + "average": 0.55 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.24, + "p95Ms": 2.08, + "maxMs": 2.08 + } + }, + { + "name": "users-create", + "routePath": "/api/users", + "path": "/api/users", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 352, + "requestsSent": 24, + "statusCodes": { + "201": 24 + }, + "latencyMs": { + "p50": 1.19, + "p95": 2.46, + "p99": 2.5, + "average": 1 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.67, + "p95Ms": 3.79, + "maxMs": 3.79 + } + }, + { + "name": "jobs-list", + "routePath": "/api/jobs", + "path": "/api/jobs", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 24, + "statusCodes": { + "200": 24 + }, + "latencyMs": { + "p50": 0.97, + "p95": 2.65, + "p99": 2.65, + "average": 0.67 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.11, + "p95Ms": 1.75, + "maxMs": 1.75 + } + }, + { + "name": "jobs-create", + "routePath": "/api/jobs", + "path": "/api/jobs", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 673, + "requestsSent": 24, + "statusCodes": { + "201": 24 + }, + "latencyMs": { + "p50": 1.77, + "p95": 3.24, + "p99": 3.25, + "average": 1.63 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.04, + "p95Ms": 1.35, + "maxMs": 1.35 + } + }, + { + "name": "proposals-list", + "routePath": "/api/proposals", + "path": "/api/proposals", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 24, + "statusCodes": { + "200": 24 + }, + "latencyMs": { + "p50": 0.81, + "p95": 4.28, + "p99": 4.31, + "average": 0.63 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 0.95, + "p95Ms": 1.62, + "maxMs": 1.62 + } + }, + { + "name": "proposals-create", + "routePath": "/api/proposals", + "path": "/api/proposals", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 552, + "requestsSent": 24, + "statusCodes": { + "201": 24 + }, + "latencyMs": { + "p50": 1.16, + "p95": 3.32, + "p99": 3.32, + "average": 1.09 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 2.94, + "p95Ms": 8.64, + "maxMs": 8.64 + } + }, + { + "name": "payments-create", + "routePath": "/api/payments", + "path": "/api/payments", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 34, + "requestsSent": 24, + "statusCodes": { + "201": 24 + }, + "latencyMs": { + "p50": 1.24, + "p95": 3.22, + "p99": 3.24, + "average": 1.17 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 3.34, + "p95Ms": 9.22, + "maxMs": 9.22 + } + }, + { + "name": "reviews-list", + "routePath": "/api/reviews", + "path": "/api/reviews", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 24, + "statusCodes": { + "200": 24 + }, + "latencyMs": { + "p50": 1.1, + "p95": 3.68, + "p99": 3.7, + "average": 0.84 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 0.92, + "p95Ms": 1.74, + "maxMs": 1.74 + } + }, + { + "name": "reviews-create", + "routePath": "/api/reviews", + "path": "/api/reviews", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 376, + "requestsSent": 24, + "statusCodes": { + "201": 24 + }, + "latencyMs": { + "p50": 1.5, + "p95": 3.33, + "p99": 3.71, + "average": 1.34 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.55, + "p95Ms": 3.18, + "maxMs": 3.18 + } + }, + { + "name": "messages-list", + "routePath": "/api/messages", + "path": "/api/messages", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 24, + "statusCodes": { + "200": 24 + }, + "latencyMs": { + "p50": 1.02, + "p95": 3.34, + "p99": 3.34, + "average": 0.92 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 0.91, + "p95Ms": 1.38, + "maxMs": 1.38 + } + }, + { + "name": "messages-create", + "routePath": "/api/messages", + "path": "/api/messages", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 357, + "requestsSent": 24, + "statusCodes": { + "201": 24 + }, + "latencyMs": { + "p50": 1.05, + "p95": 4.49, + "p99": 5.13, + "average": 1 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.67, + "p95Ms": 4.28, + "maxMs": 4.28 + } + }, + { + "name": "notifications-list", + "routePath": "/api/notifications", + "path": "/api/notifications", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 24, + "statusCodes": { + "200": 24 + }, + "latencyMs": { + "p50": 0.87, + "p95": 2.22, + "p99": 2.25, + "average": 0.46 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 0.98, + "p95Ms": 1.84, + "maxMs": 1.84 + } + }, + { + "name": "notifications-create", + "routePath": "/api/notifications", + "path": "/api/notifications", + "method": "POST", + "authProtected": false, + "payloadType": "json", + "payloadBytes": 311, + "requestsSent": 24, + "statusCodes": { + "201": 24 + }, + "latencyMs": { + "p50": 0.93, + "p95": 2.36, + "p99": 2.39, + "average": 0.42 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.2, + "p95Ms": 1.78, + "maxMs": 1.78 + } + }, + { + "name": "uploads-create", + "routePath": "/api/uploads", + "path": "/api/uploads", + "method": "POST", + "authProtected": false, + "payloadType": "multipart", + "payloadBytes": 150, + "requestsSent": 24, + "statusCodes": { + "201": 24 + }, + "latencyMs": { + "p50": 1.67, + "p95": 7.79, + "p99": 7.8, + "average": 1.59 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.16, + "p95Ms": 1.6, + "maxMs": 1.6 + } + }, + { + "name": "search-query", + "routePath": "/api/search", + "path": "/api/search?q=marketplace+payout+benchmark+plan", + "method": "GET", + "authProtected": false, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 24, + "statusCodes": { + "200": 24 + }, + "latencyMs": { + "p50": 0.9, + "p95": 3.02, + "p99": 3.07, + "average": 0.5 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.26, + "p95Ms": 2.8, + "maxMs": 2.8 + } + }, + { + "name": "admin-metrics", + "routePath": "/api/admin/metrics", + "path": "/api/admin/metrics", + "method": "GET", + "authProtected": true, + "payloadType": "none", + "payloadBytes": 0, + "requestsSent": 24, + "statusCodes": { + "200": 24 + }, + "latencyMs": { + "p50": 2.27, + "p95": 12.41, + "p99": 12.73, + "average": 3 + }, + "throughput": { + "sustainedRps": 24, + "peakRps": 24 + }, + "errorRatePct": 0, + "ttfbMs": { + "averageMs": 1.48, + "p95Ms": 1.81, + "maxMs": 1.81 + } + } + ], + "thresholdFailures": [] +} diff --git a/benchmarks/results/latest-smoke.md b/benchmarks/results/latest-smoke.md new file mode 100644 index 000000000..2e83fceb6 --- /dev/null +++ b/benchmarks/results/latest-smoke.md @@ -0,0 +1,57 @@ +# API Benchmark Summary + +- Generated at: 2026-05-22T21:41:18.494Z +- Profile: smoke +- Base URL: http://127.0.0.1:4010 +- Endpoints covered: 20 + +## Benchmark Environment + +**Hardware** +- CPU model & core count: 13th Gen Intel(R) Core(TM) i5-13600K (20 cores) +- RAM (total & available during benchmark): 31.75 GB total / 13.79 GB free +- Storage type (SSD / NVMe / HDD): unknown +- Network interface (Ethernet / WiFi / loopback): loopback +- Machine type (local workstation / cloud VM / CI runner - include instance type if cloud): local workstation +- OS & version: win32 10.0.26200 + +**Runtime** +- Node.js version (or relevant runtime): v22.18.0 +- Any resource limits applied (Docker memory cap, cgroup limits, etc.): none declared +- Other significant processes running during benchmark (yes / no - if yes, describe): unknown + +**If submitted by or with an AI agent** +- Agent or tool name: Codex +- Underlying model and version: GPT-5 +- Inference provider: OpenAI +- Orchestration framework if any: Codex desktop +- Execution mode: fully autonomous +- Did the agent have shell/tool access during execution: yes +- Did the agent have internet access during execution: yes +- Were benchmark commands run by the agent directly or handed off to the human to run: agent directly +- Any known agent constraints or sandboxing that may have affected execution: No GitHub CLI in PATH; some authenticated payout pages are browser-policy limited. + +## Endpoint Metrics + +| Endpoint | Method | Auth | Payload bytes | p50 ms | p95 ms | p99 ms | Avg ms | Sustained req/s | Peak req/s | Error % | Avg TTFB ms | p95 TTFB ms | +| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| /api/auth/register | POST | public | 90 | 1.64 | 16.71 | 16.82 | 2.59 | 24 | 24 | 0 | 3.57 | 10.04 | +| /api/auth/login | POST | public | 71 | 1.45 | 3.06 | 3.1 | 1.17 | 24 | 24 | 0 | 1.92 | 2.87 | +| /api/auth/oauth/github/callback | GET | public | 0 | 0.54 | 1.82 | 1.92 | 0.09 | 24 | 24 | 0 | 2.36 | 5.6 | +| /api/auth/refresh | POST | public | 2 | 2.26 | 4.56 | 4.61 | 2.17 | 24 | 24 | 0 | 1.97 | 3.21 | +| /api/users | GET | public | 0 | 0.95 | 2.48 | 2.69 | 0.55 | 24 | 24 | 0 | 1.24 | 2.08 | +| /api/users | POST | public | 352 | 1.19 | 2.46 | 2.5 | 1 | 24 | 24 | 0 | 1.67 | 3.79 | +| /api/jobs | GET | public | 0 | 0.97 | 2.65 | 2.65 | 0.67 | 24 | 24 | 0 | 1.11 | 1.75 | +| /api/jobs | POST | public | 673 | 1.77 | 3.24 | 3.25 | 1.63 | 24 | 24 | 0 | 1.04 | 1.35 | +| /api/proposals | GET | public | 0 | 0.81 | 4.28 | 4.31 | 0.63 | 24 | 24 | 0 | 0.95 | 1.62 | +| /api/proposals | POST | public | 552 | 1.16 | 3.32 | 3.32 | 1.09 | 24 | 24 | 0 | 2.94 | 8.64 | +| /api/payments | POST | public | 34 | 1.24 | 3.22 | 3.24 | 1.17 | 24 | 24 | 0 | 3.34 | 9.22 | +| /api/reviews | GET | public | 0 | 1.1 | 3.68 | 3.7 | 0.84 | 24 | 24 | 0 | 0.92 | 1.74 | +| /api/reviews | POST | public | 376 | 1.5 | 3.33 | 3.71 | 1.34 | 24 | 24 | 0 | 1.55 | 3.18 | +| /api/messages | GET | public | 0 | 1.02 | 3.34 | 3.34 | 0.92 | 24 | 24 | 0 | 0.91 | 1.38 | +| /api/messages | POST | public | 357 | 1.05 | 4.49 | 5.13 | 1 | 24 | 24 | 0 | 1.67 | 4.28 | +| /api/notifications | GET | public | 0 | 0.87 | 2.22 | 2.25 | 0.46 | 24 | 24 | 0 | 0.98 | 1.84 | +| /api/notifications | POST | public | 311 | 0.93 | 2.36 | 2.39 | 0.42 | 24 | 24 | 0 | 1.2 | 1.78 | +| /api/uploads | POST | public | 150 | 1.67 | 7.79 | 7.8 | 1.59 | 24 | 24 | 0 | 1.16 | 1.6 | +| /api/search?q=marketplace+payout+benchmark+plan | GET | public | 0 | 0.9 | 3.02 | 3.07 | 0.5 | 24 | 24 | 0 | 1.26 | 2.8 | +| /api/admin/metrics | GET | benchmark token | 0 | 2.27 | 12.41 | 12.73 | 3 | 24 | 24 | 0 | 1.48 | 1.81 | diff --git a/benchmarks/run.mjs b/benchmarks/run.mjs new file mode 100644 index 000000000..7f3dd1658 --- /dev/null +++ b/benchmarks/run.mjs @@ -0,0 +1,638 @@ +import autocannon from "autocannon"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import process from "node:process"; +import { performance } from "node:perf_hooks"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(__dirname, ".."); +const resultsDir = path.join(__dirname, "results"); +const fixturePath = path.join(__dirname, "fixtures", "upload-sample.txt"); +const thresholdPath = path.join(__dirname, "thresholds.json"); +const envTemplatePath = path.join(__dirname, ".env.benchmark.example"); + +function parseArgs(argv) { + const options = { + profile: undefined, + verify: false + }; + + for (const arg of argv) { + if (arg === "--verify") { + options.verify = true; + continue; + } + + if (arg.startsWith("--profile=")) { + options.profile = arg.split("=")[1] || undefined; + } + } + + return options; +} + +async function loadEnvFile(filePath) { + try { + const raw = await fs.readFile(filePath, "utf8"); + for (const line of raw.split(/\r?\n/u)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) { + continue; + } + + const separator = trimmed.indexOf("="); + if (separator === -1) { + continue; + } + + const key = trimmed.slice(0, separator).trim(); + const value = trimmed.slice(separator + 1).trim(); + if (!(key in process.env)) { + process.env[key] = value; + } + } + } catch (error) { + if (error?.code !== "ENOENT") { + throw error; + } + } +} + +function parseNumber(value, fallback) { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +function round(value, digits = 2) { + return Number(value.toFixed(digits)); +} + +function percentile(values, target) { + if (values.length === 0) { + return 0; + } + + const sorted = [...values].sort((left, right) => left - right); + const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil((target / 100) * sorted.length) - 1)); + return sorted[index]; +} + +function repeatSentence(sentence, count) { + return Array.from({ length: count }, () => sentence).join(" "); +} + +function getProfileSettings(profile) { + if (profile === "smoke") { + return { + amount: parseNumber(process.env.BENCHMARK_AMOUNT, 24), + connections: parseNumber(process.env.BENCHMARK_CONCURRENCY, 2), + sampleInt: 1000, + ttfbSamples: parseNumber(process.env.BENCHMARK_TTFB_SAMPLES, 3) + }; + } + + return { + amount: parseNumber(process.env.BENCHMARK_AMOUNT, 80), + connections: parseNumber(process.env.BENCHMARK_CONCURRENCY, 6), + sampleInt: 1000, + ttfbSamples: parseNumber(process.env.BENCHMARK_TTFB_SAMPLES, 5) + }; +} + +function buildEndpointDefinitions(adminToken) { + return [ + { + name: "auth-register", + routePath: "/api/auth/register", + path: "/api/auth/register", + method: "POST", + body: { + email: "benchmark.register@example.com", + password: "benchmark-pass-123", + role: "client" + } + }, + { + name: "auth-login", + routePath: "/api/auth/login", + path: "/api/auth/login", + method: "POST", + body: { + email: "benchmark.login@example.com", + password: "benchmark-pass-123" + } + }, + { + name: "auth-oauth-callback", + routePath: "/api/auth/oauth/github/callback", + path: "/api/auth/oauth/github/callback", + method: "GET" + }, + { + name: "auth-refresh", + routePath: "/api/auth/refresh", + path: "/api/auth/refresh", + method: "POST", + body: {} + }, + { + name: "users-list", + routePath: "/api/users", + path: "/api/users", + method: "GET" + }, + { + name: "users-create", + routePath: "/api/users", + path: "/api/users", + method: "POST", + body: { + email: "benchmark.user@example.com", + fullName: "Benchmark User", + role: "freelancer", + bio: repeatSentence("Senior marketplace engineer with payment and search experience.", 3), + isVerified: true, + skills: ["node", "react", "payments", "postgres"] + } + }, + { + name: "jobs-list", + routePath: "/api/jobs", + path: "/api/jobs", + method: "GET" + }, + { + name: "jobs-create", + routePath: "/api/jobs", + path: "/api/jobs", + method: "POST", + body: { + title: "Marketplace payout reliability audit", + description: repeatSentence( + "Build a resilient freelance workflow with audit logging, payout milestones, and review automation.", + 5 + ), + budgetMin: 2500, + budgetMax: 8000, + categoryId: "fintech-platform", + skills: ["node", "postgres", "stripe", "testing"] + } + }, + { + name: "proposals-list", + routePath: "/api/proposals", + path: "/api/proposals", + method: "GET" + }, + { + name: "proposals-create", + routePath: "/api/proposals", + path: "/api/proposals", + method: "POST", + body: { + coverLetter: repeatSentence( + "I have shipped marketplace workflows, notification systems, and payment integrations at production scale.", + 4 + ), + bidAmount: 4200, + estDuration: "3 weeks", + jobId: "job_benchmark_001", + freelancerId: "usr_benchmark_freelancer" + } + }, + { + name: "payments-create", + routePath: "/api/payments", + path: "/api/payments", + method: "POST", + body: { + amount: 129900, + currency: "usd" + } + }, + { + name: "reviews-list", + routePath: "/api/reviews", + path: "/api/reviews", + method: "GET" + }, + { + name: "reviews-create", + routePath: "/api/reviews", + path: "/api/reviews", + method: "POST", + body: { + rating: 5, + comment: repeatSentence( + "Fast communication, clear specs, and predictable delivery made this project easy to manage.", + 3 + ), + reviewerId: "usr_benchmark_client", + revieweeId: "usr_benchmark_freelancer" + } + }, + { + name: "messages-list", + routePath: "/api/messages", + path: "/api/messages", + method: "GET" + }, + { + name: "messages-create", + routePath: "/api/messages", + path: "/api/messages", + method: "POST", + body: { + body: repeatSentence( + "Sharing a milestone update with next steps, blockers, and a request for approval on scope.", + 3 + ), + senderId: "usr_benchmark_client", + receiverId: "usr_benchmark_freelancer" + } + }, + { + name: "notifications-list", + routePath: "/api/notifications", + path: "/api/notifications", + method: "GET" + }, + { + name: "notifications-create", + routePath: "/api/notifications", + path: "/api/notifications", + method: "POST", + body: { + userId: "usr_benchmark_client", + title: "Proposal awaiting review", + body: repeatSentence( + "A new proposal arrived and requires review before the response window closes.", + 3 + ) + } + }, + { + name: "uploads-create", + routePath: "/api/uploads", + path: "/api/uploads", + method: "POST", + form: { + file: { + type: "file", + path: fixturePath, + options: { + filename: "upload-sample.txt", + contentType: "text/plain" + } + } + } + }, + { + name: "search-query", + routePath: "/api/search", + path: "/api/search?q=marketplace+payout+benchmark+plan", + method: "GET" + }, + { + name: "admin-metrics", + routePath: "/api/admin/metrics", + path: "/api/admin/metrics", + method: "GET", + headers: { + authorization: `Bearer ${adminToken}` + }, + authProtected: true + } + ]; +} + +function buildEnvironmentSummary(profile, baseUrl, localTarget) { + const cpus = os.cpus(); + + return { + profile, + baseUrl, + target: localTarget ? "local" : "remote", + hardware: { + cpuModel: cpus[0]?.model ?? "unknown", + coreCount: cpus.length, + ramTotalGb: round(os.totalmem() / 1024 / 1024 / 1024, 2), + ramFreeGb: round(os.freemem() / 1024 / 1024 / 1024, 2), + storageType: "unknown", + networkInterface: localTarget ? "loopback" : "unknown", + machineType: "local workstation" + }, + runtime: { + nodeVersion: process.version, + os: `${os.platform()} ${os.release()}`, + resourceLimits: "none declared", + backgroundProcesses: "unknown" + }, + aiAgent: { + toolName: "Codex", + model: "GPT-5", + provider: "OpenAI", + orchestration: "Codex desktop", + executionMode: "fully autonomous", + shellAccess: true, + internetAccess: true, + benchmarkExecutedByAgent: true, + knownConstraints: + "No GitHub CLI in PATH; some authenticated payout pages are browser-policy limited." + } + }; +} + +async function measureTtfb(baseUrl, endpoint, samples) { + const timings = []; + + for (let index = 0; index < samples; index += 1) { + const headers = new Headers(endpoint.headers ?? {}); + let body; + + if (endpoint.form) { + const fileBuffer = await fs.readFile(fixturePath); + const formData = new FormData(); + formData.set("file", new Blob([fileBuffer], { type: "text/plain" }), "upload-sample.txt"); + body = formData; + } else if (endpoint.method !== "GET" && endpoint.body !== undefined) { + headers.set("content-type", "application/json"); + body = JSON.stringify(endpoint.body); + } + + const startedAt = performance.now(); + const response = await fetch(`${baseUrl}${endpoint.path}`, { + method: endpoint.method, + headers, + body + }); + timings.push(performance.now() - startedAt); + await response.arrayBuffer(); + } + + return { + averageMs: round(timings.reduce((sum, value) => sum + value, 0) / timings.length), + p95Ms: round(percentile(timings, 95)), + maxMs: round(Math.max(...timings)) + }; +} + +async function benchmarkEndpoint(baseUrl, endpoint, settings) { + const statusCodes = {}; + const responseSamples = []; + const payloadBytes = endpoint.form + ? (await fs.stat(fixturePath)).size + : endpoint.body === undefined + ? 0 + : Buffer.byteLength(JSON.stringify(endpoint.body)); + + const instance = autocannon({ + url: `${baseUrl}${endpoint.path}`, + method: endpoint.method, + amount: settings.amount, + connections: settings.connections, + sampleInt: settings.sampleInt, + timeout: 10, + renderProgressBar: false, + renderResultsTable: false, + renderLatencyTable: false, + headers: endpoint.form + ? { ...(endpoint.headers ?? {}) } + : { + accept: "application/json", + ...(endpoint.method !== "GET" && endpoint.body !== undefined + ? { "content-type": "application/json" } + : {}), + ...(endpoint.headers ?? {}) + }, + body: + endpoint.form || endpoint.method === "GET" || endpoint.body === undefined + ? undefined + : JSON.stringify(endpoint.body), + form: endpoint.form + }); + + instance.on("response", (client, statusCode, responseBytes, responseTime) => { + statusCodes[statusCode] = (statusCodes[statusCode] ?? 0) + 1; + responseSamples.push(responseTime); + }); + + const result = await instance; + const completedRequests = result.requests.total || responseSamples.length || 1; + const errorRatePct = round(((result.errors + result.non2xx) / completedRequests) * 100); + const ttfb = await measureTtfb(baseUrl, endpoint, settings.ttfbSamples); + + return { + name: endpoint.name, + routePath: endpoint.routePath, + path: endpoint.path, + method: endpoint.method, + authProtected: Boolean(endpoint.authProtected), + payloadType: endpoint.form ? "multipart" : endpoint.method === "GET" ? "none" : "json", + payloadBytes, + requestsSent: completedRequests, + statusCodes, + latencyMs: { + p50: round(percentile(responseSamples, 50)), + p95: round(percentile(responseSamples, 95)), + p99: round(percentile(responseSamples, 99)), + average: round(result.latency.average) + }, + throughput: { + sustainedRps: round(result.requests.average), + peakRps: round(result.requests.max) + }, + errorRatePct, + ttfbMs: ttfb + }; +} + +function evaluateThresholds(summary, thresholds) { + const profileThresholds = thresholds.profiles?.[summary.profile] ?? {}; + const failures = []; + + for (const result of summary.results) { + const key = `${result.method} ${result.routePath}`; + const threshold = profileThresholds[key]; + if (!threshold) { + continue; + } + + if (result.latencyMs.p99 > threshold.p99Ms) { + failures.push( + `${key} p99 ${result.latencyMs.p99} ms exceeded smoke threshold ${threshold.p99Ms} ms` + ); + } + + if (typeof threshold.maxErrorRatePct === "number" && result.errorRatePct > threshold.maxErrorRatePct) { + failures.push( + `${key} error rate ${result.errorRatePct}% exceeded ${threshold.maxErrorRatePct}%` + ); + } + } + + return failures; +} + +function renderMarkdown(summary) { + const lines = [ + "# API Benchmark Summary", + "", + `- Generated at: ${summary.generatedAt}`, + `- Profile: ${summary.profile}`, + `- Base URL: ${summary.baseUrl}`, + `- Endpoints covered: ${summary.results.length}`, + "", + "## Benchmark Environment", + "", + "**Hardware**", + `- CPU model & core count: ${summary.environment.hardware.cpuModel} (${summary.environment.hardware.coreCount} cores)`, + `- RAM (total & available during benchmark): ${summary.environment.hardware.ramTotalGb} GB total / ${summary.environment.hardware.ramFreeGb} GB free`, + `- Storage type (SSD / NVMe / HDD): ${summary.environment.hardware.storageType}`, + `- Network interface (Ethernet / WiFi / loopback): ${summary.environment.hardware.networkInterface}`, + `- Machine type (local workstation / cloud VM / CI runner - include instance type if cloud): ${summary.environment.hardware.machineType}`, + `- OS & version: ${summary.environment.runtime.os}`, + "", + "**Runtime**", + `- Node.js version (or relevant runtime): ${summary.environment.runtime.nodeVersion}`, + `- Any resource limits applied (Docker memory cap, cgroup limits, etc.): ${summary.environment.runtime.resourceLimits}`, + `- Other significant processes running during benchmark (yes / no - if yes, describe): ${summary.environment.runtime.backgroundProcesses}`, + "", + "**If submitted by or with an AI agent**", + `- Agent or tool name: ${summary.environment.aiAgent.toolName}`, + `- Underlying model and version: ${summary.environment.aiAgent.model}`, + `- Inference provider: ${summary.environment.aiAgent.provider}`, + `- Orchestration framework if any: ${summary.environment.aiAgent.orchestration}`, + `- Execution mode: ${summary.environment.aiAgent.executionMode}`, + `- Did the agent have shell/tool access during execution: ${summary.environment.aiAgent.shellAccess ? "yes" : "no"}`, + `- Did the agent have internet access during execution: ${summary.environment.aiAgent.internetAccess ? "yes" : "no"}`, + `- Were benchmark commands run by the agent directly or handed off to the human to run: ${summary.environment.aiAgent.benchmarkExecutedByAgent ? "agent directly" : "handed off"}`, + `- Any known agent constraints or sandboxing that may have affected execution: ${summary.environment.aiAgent.knownConstraints}`, + "", + "## Endpoint Metrics", + "", + "| Endpoint | Method | Auth | Payload bytes | p50 ms | p95 ms | p99 ms | Avg ms | Sustained req/s | Peak req/s | Error % | Avg TTFB ms | p95 TTFB ms |", + "| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |" + ]; + + for (const result of summary.results) { + lines.push( + `| ${result.path} | ${result.method} | ${result.authProtected ? "benchmark token" : "public"} | ${result.payloadBytes} | ${result.latencyMs.p50} | ${result.latencyMs.p95} | ${result.latencyMs.p99} | ${result.latencyMs.average} | ${result.throughput.sustainedRps} | ${result.throughput.peakRps} | ${result.errorRatePct} | ${result.ttfbMs.averageMs} | ${result.ttfbMs.p95Ms} |` + ); + } + + if (summary.thresholdFailures.length > 0) { + lines.push("", "## Threshold Failures", ""); + for (const failure of summary.thresholdFailures) { + lines.push(`- ${failure}`); + } + } + + return `${lines.join("\n")}\n`; +} + +async function ensureResultsDir() { + await fs.mkdir(resultsDir, { recursive: true }); +} + +async function startLocalServer(port) { + process.env.BENCHMARK_MODE = process.env.BENCHMARK_MODE ?? "1"; + process.env.NODE_ENV = "benchmark"; + process.env.PORT = String(port); + + const [{ createApp }, { connectDb }] = await Promise.all([ + import("../apps/api/src/app.js"), + import("../apps/api/src/config/db.js") + ]); + await connectDb(); + const app = createApp(); + const server = app.listen(port); + + await new Promise((resolve, reject) => { + server.once("listening", resolve); + server.once("error", reject); + }); + + return server; +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + await loadEnvFile(path.join(__dirname, ".env.benchmark")); + await loadEnvFile(envTemplatePath); + + const profile = options.profile ?? process.env.BENCHMARK_PROFILE ?? "full"; + const settings = getProfileSettings(profile); + const localTarget = (process.env.BENCHMARK_TARGET ?? "local").toLowerCase() === "local"; + const port = parseNumber(process.env.BENCHMARK_PORT, 4010); + const baseUrl = localTarget + ? `http://127.0.0.1:${port}` + : (process.env.BENCHMARK_BASE_URL || "").replace(/\/$/u, ""); + + if (!baseUrl) { + throw new Error("BENCHMARK_BASE_URL must be set when BENCHMARK_TARGET is not local."); + } + + let server; + + try { + if (localTarget) { + server = await startLocalServer(port); + } + + const thresholds = JSON.parse(await fs.readFile(thresholdPath, "utf8")); + let adminToken = process.env.BENCHMARK_ADMIN_TOKEN ?? ""; + if (!adminToken && !localTarget) { + throw new Error("BENCHMARK_ADMIN_TOKEN must be set for remote benchmark targets."); + } + if (!adminToken) { + const { signAccessToken } = await import("../apps/api/src/utils/jwt.js"); + adminToken = signAccessToken({ + sub: "benchmark-admin", + role: "admin" + }); + } + + const summary = { + generatedAt: new Date().toISOString(), + profile, + baseUrl, + environment: buildEnvironmentSummary(profile, baseUrl, localTarget), + results: [], + thresholdFailures: [] + }; + + for (const endpoint of buildEndpointDefinitions(adminToken)) { + summary.results.push(await benchmarkEndpoint(baseUrl, endpoint, settings)); + } + + summary.thresholdFailures = evaluateThresholds(summary, thresholds); + + await ensureResultsDir(); + const jsonPath = path.join(resultsDir, `latest-${profile}.json`); + const markdownPath = path.join(resultsDir, `latest-${profile}.md`); + + await fs.writeFile(jsonPath, `${JSON.stringify(summary, null, 2)}\n`); + await fs.writeFile(markdownPath, renderMarkdown(summary)); + + process.stdout.write(`Wrote ${path.relative(repoRoot, jsonPath)}\n`); + process.stdout.write(`Wrote ${path.relative(repoRoot, markdownPath)}\n`); + + if (options.verify && summary.thresholdFailures.length > 0) { + throw new Error(summary.thresholdFailures.join("\n")); + } + } finally { + if (server) { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +}); diff --git a/benchmarks/thresholds.json b/benchmarks/thresholds.json new file mode 100644 index 000000000..1c7e1e3f8 --- /dev/null +++ b/benchmarks/thresholds.json @@ -0,0 +1,26 @@ +{ + "profiles": { + "smoke": { + "POST /api/auth/register": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "POST /api/auth/login": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "GET /api/auth/oauth/github/callback": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "POST /api/auth/refresh": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "GET /api/users": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "POST /api/users": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "GET /api/jobs": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "POST /api/jobs": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "GET /api/proposals": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "POST /api/proposals": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "POST /api/payments": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "GET /api/reviews": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "POST /api/reviews": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "GET /api/messages": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "POST /api/messages": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "GET /api/notifications": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "POST /api/notifications": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "POST /api/uploads": { "p99Ms": 300, "maxErrorRatePct": 0 }, + "GET /api/search": { "p99Ms": 250, "maxErrorRatePct": 0 }, + "GET /api/admin/metrics": { "p99Ms": 250, "maxErrorRatePct": 0 } + } + } +} diff --git a/demos/api-benchmark-demo.webm b/demos/api-benchmark-demo.webm new file mode 100644 index 000000000..2c8e5434c Binary files /dev/null and b/demos/api-benchmark-demo.webm differ diff --git a/package-lock.json b/package-lock.json index a19a99281..71ee9e036 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,10 @@ "workspaces": [ "apps/*", "packages/*" - ] + ], + "devDependencies": { + "autocannon": "^8.0.0" + } }, "apps/api": { "name": "@freelanceflow/api", @@ -35,6 +38,24 @@ "typescript": "5.6.3" } }, + "node_modules/@assemblyscript/loader": { + "version": "0.19.23", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.19.23.tgz", + "integrity": "sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@emnapi/runtime": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", @@ -527,6 +548,16 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@minimistjs/subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@minimistjs/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha512-Q/ONBiM2zNeYUy0mVSO44mWWKYM3UHuEK43PKIOzJCbvUnPoMH1K+gk3cf1kgnCVJFlWmddahQQCmrmBGlk9jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.1.0" + } + }, "node_modules/@next/env": { "version": "16.2.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", @@ -771,6 +802,32 @@ "node": ">= 0.6" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -783,6 +840,69 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autocannon": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/autocannon/-/autocannon-8.0.0.tgz", + "integrity": "sha512-fMMcWc2JPFcUaqHeR6+PbmEpTxCrPZyBUM95oG4w3ngJ8NfBNas/ZXA+pTHXLqJ0UlFVTcy05GC25WxKx/M20A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@minimistjs/subarg": "^1.0.0", + "chalk": "^4.1.0", + "char-spinner": "^1.0.1", + "cli-table3": "^0.6.0", + "color-support": "^1.1.1", + "cross-argv": "^2.0.0", + "form-data": "^4.0.0", + "has-async-hooks": "^1.0.0", + "hdr-histogram-js": "^3.0.0", + "hdr-histogram-percentiles-obj": "^3.0.0", + "http-parser-js": "^0.5.2", + "hyperid": "^3.0.0", + "lodash.chunk": "^4.2.0", + "lodash.clonedeep": "^4.5.0", + "lodash.flatten": "^4.4.0", + "manage-path": "^2.0.0", + "on-net-listen": "^1.1.1", + "pretty-bytes": "^5.4.1", + "progress": "^2.0.3", + "reinterval": "^1.1.0", + "retimer": "^3.0.0", + "semver": "^7.3.2", + "timestring": "^6.0.0" + }, + "bin": { + "autocannon": "autocannon.js" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.29", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", @@ -819,6 +939,31 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -900,12 +1045,95 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-spinner": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/char-spinner/-/char-spinner-1.0.1.tgz", + "integrity": "sha512-acv43vqJ0+N0rD+Uw3pDHSxP30FHrywu2NO6/wBaHChJIizpDeBUd6NjqhNhy9LGaEAhZAXn46QzmlAvIWd16g==", + "dev": true, + "license": "ISC" + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -974,6 +1202,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cross-argv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cross-argv/-/cross-argv-2.0.0.tgz", + "integrity": "sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -990,6 +1225,16 @@ "ms": "2.0.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1048,6 +1293,13 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1087,6 +1339,22 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1181,6 +1449,23 @@ "node": ">= 0.8" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1272,6 +1557,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-async-hooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-async-hooks/-/has-async-hooks-1.0.0.tgz", + "integrity": "sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1284,6 +1586,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", @@ -1296,6 +1614,28 @@ "node": ">= 0.4" } }, + "node_modules/hdr-histogram-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-3.0.1.tgz", + "integrity": "sha512-l3GSdZL1Jr1C0kyb461tUjEdrRPZr8Qry7jByltf5JGrA0xvqOSrxRBfcrJqqV/AMEtqqhHhC6w8HW0gn76tRQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@assemblyscript/loader": "^0.19.21", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true, + "license": "MIT" + }, "node_modules/helmet": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", @@ -1325,6 +1665,25 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/hyperid": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.3.0.tgz", + "integrity": "sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "uuid": "^8.3.2", + "uuid-parse": "^1.1.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1337,6 +1696,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1352,6 +1732,16 @@ "node": ">= 0.10" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -1401,6 +1791,27 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lodash.chunk": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz", + "integrity": "sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -1443,6 +1854,13 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/manage-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/manage-path/-/manage-path-2.0.0.tgz", + "integrity": "sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A==", + "dev": true, + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1512,6 +1930,16 @@ "node": ">= 0.6" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1650,6 +2078,23 @@ "node": ">= 0.8" } }, + "node_modules/on-net-listen": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/on-net-listen/-/on-net-listen-1.1.2.tgz", + "integrity": "sha512-y1HRYy8s/RlcBvDUwKXSmkODMdx4KSuIvloCnQYJ2LdBBC1asY4HtfhXwe3UWknLakATZDnbzht2Ijw3M1EqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=9.4.0 || ^8.9.4" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1699,6 +2144,19 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prisma": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", @@ -1719,6 +2177,16 @@ "fsevents": "2.3.3" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1806,6 +2274,20 @@ "node": ">= 6" } }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/retimer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/retimer/-/retimer-3.0.0.tgz", + "integrity": "sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2053,6 +2535,34 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -2076,6 +2586,29 @@ } } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/timestring": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/timestring/-/timestring-6.0.0.tgz", + "integrity": "sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2155,6 +2688,24 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uuid-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", + "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==", + "dev": true, + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 675e6e69d..fb30ead02 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,12 @@ ], "scripts": { "build": "echo \"Run package-specific builds (e.g. npm run build -w apps/web)\"", + "benchmark": "node benchmarks/run.mjs", + "benchmark:smoke": "node benchmarks/run.mjs --profile=smoke --verify", "lint": "echo \"No root lint configured\"", "test": "npm run test -w apps/api" + }, + "devDependencies": { + "autocannon": "^8.0.0" } }