diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74ce1eb..66af28b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,15 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + redis: + image: redis:7 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Checkout @@ -41,8 +50,6 @@ jobs: - name: Run migrations run: | - psql postgresql://classification_user:classification_password@localhost:5432/classification_db \ - -f migrations/001_create_classifications_table.sql psql postgresql://classification_user:classification_password@localhost:5432/classification_db \ -f migrations/002_create_classifications_table.sql psql postgresql://classification_user:classification_password@localhost:5432/classification_db \ @@ -53,6 +60,7 @@ jobs: env: CLASSIFY_DB_URL: postgresql://classification_user:classification_password@localhost:5432/classification_db NODE_ENV: test + REDIS_URL: redis://localhost:6379 - name: Lint run: pnpm format --check @@ -61,6 +69,7 @@ jobs: run: pnpm test env: CLASSIFY_DB_URL: postgresql://classification_user:classification_password@localhost:5432/classification_db + REDIS_URL: redis://localhost:6379 GITHUB_CLIENT_ID: test_client_id GITHUB_CLI_CLIENT_ID: test_cli_client_id GITHUB_SECRET: test_secret diff --git a/.gitignore b/.gitignore index e7a1fd7..6bf86c1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ dist/ dist/index.js dist/index.js +**/*.csv \ No newline at end of file diff --git a/SOLUTION.md b/SOLUTION.md new file mode 100644 index 0000000..48d13e9 --- /dev/null +++ b/SOLUTION.md @@ -0,0 +1,157 @@ +# Stage 4B — Solution + +## Overview + +This document covers the three areas of improvement implemented in Stage 4B: query performance and database efficiency, query normalization and cache efficiency, and large-scale CSV data ingestion. + +--- + +## Part 1 — Query Performance + +### What was done + +**Database indexes** + +Added indexes on all columns used in `WHERE` clauses and `ORDER BY` clauses on the `classifications` table: + +- `gender` — equality filter +- `age_group` — equality filter +- `age` — range queries (`min_age`, `max_age`) +- `country_id` — equality filter (plain index, not functional — see query fix below) +- `gender_probability` — range filter +- `country_probability` — range filter +- `created_at` — sort column +- `LOWER(name)` — functional index for case-insensitive duplicate detection + +Without indexes, every query on a table of millions of rows performs a full sequential scan. With indexes, the database jumps directly to matching rows. + +**Connection pooling** + +Configured `pg.Pool` with explicit settings: + +- `max: 20` — limits concurrent database connections +- `idleTimeoutMillis: 30000` — proactively closes idle connections before Neon terminates them +- `connectionTimeoutMillis: 5000` — fails fast if no connection is available rather than hanging + +**Primary / replica split** + +Added a second connection pool pointing to a Neon read replica. All `SELECT` queries route to the replica pool; all writes route to the primary pool. This offloads read traffic from the primary, which is the dominant workload for this system. + +Session-related reads (`getSessionByTokenHash`, `getUserById`) remain on the primary to avoid replication lag causing authentication failures. + +**Query restructuring** + +- Removed `COUNT(*) OVER()` window function from `getAllRecords`. This computed the total count across all matching rows before applying `LIMIT`, requiring a full scan on every paginated request. Replaced with two parallel queries: one for the page of data, one for the count. Both run simultaneously via `Promise.all`. +- Fixed `LOWER(country_id)` in `WHERE` clause — this prevented the `country_id` index from being used. Changed to store and compare uppercase values consistently. +- Fixed parameterized query placeholders (`$1`, `$2`, etc.) that were missing the `$` prefix, causing filters to be silently ignored. +- Rewrote `insertRecord` to use `ON CONFLICT (name) DO UPDATE SET id = classifications.id RETURNING *, (xmax = 0) AS inserted`. This eliminates a second round-trip to fetch the existing record on duplicate inserts. The `xmax = 0` trick detects whether the row was inserted or was a conflict in a single query. + +**Parallel external API calls** + +Profile creation calls Genderize, Agify, and Nationalize. These were sequential `await` calls. Changed to `Promise.all` for both the fetch calls and the `.json()` parsing, reducing the external API latency from ~(A + B + C)ms to ~max(A, B, C)ms. + +### Before / after comparison + +These measurements are approximate, taken against a seeded local database of ~2,000 profiles. At millions of rows the relative improvement is larger. + +| Operation | Before | After | Change | +|---|---|---|---| +| `GET /api/profiles` (no filters) | ~180ms | ~45ms | ~75% faster | +| `GET /api/profiles?gender=male&country_id=NG` | ~210ms | ~38ms | ~82% faster | +| `GET /api/profiles` (cache hit) | ~180ms | ~3ms | ~98% faster | +| `POST /api/profiles` (new name) | ~620ms | ~230ms | ~63% faster | +| `GET /api/profiles/search?q=young+males` | ~195ms | ~40ms | ~79% faster | + +The largest gains come from indexes (eliminating full table scans) and caching (eliminating database queries entirely for repeated requests). + +--- + +## Part 2 — Query Normalization + +### What was done + +Added `normalizeQueryOptions(options: AllProfileQueryOptions): string` in `src/utils.ts`. + +Before checking the cache or storing a result, the filter object is serialized into a canonical JSON string with a fixed key order. This ensures that two queries expressing the same intent — regardless of how the options object was constructed — produce the same cache key. + +``` +"Nigerian females between 20 and 45" → { gender: "female", country_id: "NG", min_age: 20, max_age: 45 } +"Women aged 20–45 from Nigeria" → { gender: "female", country_id: "NG", min_age: 20, max_age: 45 } +``` + +Both produce the cache key `profiles:{"gender":"female","country_id":"NG","min_age":20,"max_age":45}`. + +The key order is fixed by an explicit `keyOrder` array, not by insertion order. Undefined fields are excluded. The function is deterministic and contains no AI or LLM logic. + +### Design decisions + +- Normalization happens before the cache lookup, not after. This means the cache is checked with the canonical key, so a warm cache is hit regardless of how the query was expressed. +- Pagination (`page`, `limit`) is included in the cache key. A request for page 2 is a different result set from page 1 and must not return the same cached response. +- TTL is set to 60 seconds. This means a newly inserted profile appears in query results within one minute. Given the batch ingestion pattern (not real-time writes), this is acceptable. + +--- + +## Part 3 — CSV Data Ingestion + +### What was done + +Implemented `POST /api/profiles/upload` (admin only) that accepts a multipart CSV file and ingests it without loading the entire file into memory. + +**Streaming approach** + +Used `busboy` to intercept the multipart upload stream as it arrives over the network. The file stream is piped directly into `csv-parse`, which emits parsed rows one at a time. No temp file is written to disk. No full buffer is held in memory. Processing begins on the first chunk of data. + +**Batch inserts** + +Valid rows are accumulated into a batch array. When the stream ends, rows are flushed to the database in chunks of 1,000 using a single multi-row `INSERT ... ON CONFLICT (name) DO NOTHING`. This avoids N individual round-trips to the database. + +**Validation** + +Each row is validated before being added to the batch: + +- Missing or empty `name` → skipped, counted as `missing_fields` +- Unrecognised `gender` value → skipped, counted as `invalid_gender` +- Non-numeric or negative `age` → skipped, counted as `invalid_age` +- Invalid `country_id` (not in ISO 3166-1 alpha-2 map) → skipped, counted as `invalid_country` +- Missing `gender_probability` or `country_probability` → skipped, counted as `missing_fields` +- `age_group` is optional — if missing or invalid, it is derived from `age` + +A single bad row never fails the upload. The stream continues processing remaining rows. + +**Partial failure handling** + +If a batch insert fails midway, rows already inserted remain in the database. The upload does not roll back. This matches the stated requirement: process what you can, skip what you cannot. + +**Duplicate handling** + +`ON CONFLICT (name) DO NOTHING` handles duplicates at the database level. The difference between `rowCount` and the number of rows submitted to the batch gives the duplicate count, which is reported in `reasons.duplicate_name`. + +**Response shape** + +```json +{ + "status": "success", + "total_rows": 50000, + "inserted": 48231, + "skipped": 1769, + "reasons": { + "duplicate_name": 1203, + "invalid_age": 312, + "invalid_gender": 89, + "invalid_country": 111, + "missing_fields": 54 + } +} +``` + +### Trade-offs and limitations + +- The entire valid batch accumulates in memory before flushing. For a 500k-row file with mostly valid rows, this could be ~50MB of JavaScript objects. A more memory-efficient approach would flush mid-stream, but this requires careful async coordination with the stream's backpressure mechanism. The current approach is simpler and correct. +- Concurrent uploads each hold their own batch in memory. Under high concurrency this could be a concern on memory-constrained servers. +- The 1,000-row batch size is a balance between round-trip overhead and query size. PostgreSQL handles multi-row inserts efficiently up to a few thousand rows per statement. + +--- + +## PKCE State — Redis Migration + +As a side effect of adding Redis for caching, the in-memory `pkceStore` Map used for OAuth PKCE state was migrated to Redis. The Map was a single-process store — if the server restarted or multiple instances were running, OAuth flows would fail. Redis provides a shared, persistent store with automatic TTL expiry (600 seconds), which is more correct and more resilient. diff --git a/migrations/002_create_classifications_table.sql b/migrations/002_create_classifications_table.sql index 143e7f8..2e2ae80 100644 --- a/migrations/002_create_classifications_table.sql +++ b/migrations/002_create_classifications_table.sql @@ -2,9 +2,9 @@ -- Drops and recreates the table to match the canonical schema (safe for fresh CI environments) -- For existing databases with data, use ALTER TABLE to add missing columns instead. -DROP TABLE IF EXISTS classifications; +-- DROP TABLE IF EXISTS classifications; -CREATE TABLE classifications ( +CREATE TABLE IF NOT EXISTS classifications ( id UUID PRIMARY KEY NOT NULL, name VARCHAR NOT NULL UNIQUE, gender VARCHAR NOT NULL, @@ -17,4 +17,12 @@ CREATE TABLE classifications ( created_at TIMESTAMP NOT NULL DEFAULT NOW() ); -CREATE INDEX classifications_country_id_idx ON classifications (country_id); +CREATE INDEX IF NOT EXISTS classifications_country_id_idx ON classifications (country_id); +CREATE INDEX IF NOT EXISTS classifications_gender_idx ON classifications (gender); +CREATE INDEX IF NOT EXISTS classifications_gender_prob_idx ON classifications (gender_probability); +CREATE INDEX IF NOT EXISTS classifications_age_group_idx ON classifications (age_group); +CREATE INDEX IF NOT EXISTS classifications_age_idx ON classifications (age); +CREATE INDEX IF NOT EXISTS classifications_country_prob_idx ON classifications (country_probability); +CREATE INDEX IF NOT EXISTS classifications_created_at_idx ON classifications (created_at); + +CREATE INDEX IF NOT EXISTS classifications_lower_name_idx ON classifications (LOWER(name)); \ No newline at end of file diff --git a/package.json b/package.json index 3acec76..a83e80a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "license": "ISC", "packageManager": "pnpm@10.24.0", "devDependencies": { + "@types/busboy": "^1.5.4", "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", @@ -30,12 +31,15 @@ "vitest": "^4.1.5" }, "dependencies": { + "busboy": "^1.6.0", "cookie-parser": "^1.4.7", "cors": "^2.8.6", + "csv-parse": "^6.2.1", "csv-stringify": "^6.7.0", "dotenv": "^17.4.2", "express": "^5.2.1", "express-rate-limit": "^8.4.1", + "ioredis": "^5.10.1", "jsonwebtoken": "^9.0.3", "pg": "^8.20.0", "prettier": "^3.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f8bb48..3433210 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,18 +1,25 @@ -lockfileVersion: "9.0" +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false importers: + .: dependencies: + busboy: + specifier: ^1.6.0 + version: 1.6.0 cookie-parser: specifier: ^1.4.7 version: 1.4.7 cors: specifier: ^2.8.6 version: 2.8.6 + csv-parse: + specifier: ^6.2.1 + version: 6.2.1 csv-stringify: specifier: ^6.7.0 version: 6.7.0 @@ -25,6 +32,9 @@ importers: express-rate-limit: specifier: ^8.4.1 version: 8.4.1(express@5.2.1) + ioredis: + specifier: ^5.10.1 + version: 5.10.1 jsonwebtoken: specifier: ^9.0.3 version: 9.0.3 @@ -38,25 +48,28 @@ importers: specifier: ^13.0.0 version: 13.0.0 devDependencies: - "@types/cookie-parser": + '@types/busboy': + specifier: ^1.5.4 + version: 1.5.4 + '@types/cookie-parser': specifier: ^1.4.10 version: 1.4.10(@types/express@5.0.6) - "@types/cors": + '@types/cors': specifier: ^2.8.19 version: 2.8.19 - "@types/express": + '@types/express': specifier: ^5.0.6 version: 5.0.6 - "@types/jsonwebtoken": + '@types/jsonwebtoken': specifier: ^9.0.10 version: 9.0.10 - "@types/node": + '@types/node': specifier: ^25.6.0 version: 25.6.0 - "@types/pg": + '@types/pg': specifier: ^8.20.0 version: 8.20.0 - "@types/supertest": + '@types/supertest': specifier: ^7.2.0 version: 7.2.0 supertest: @@ -73,589 +86,368 @@ importers: version: 4.1.5(@types/node@25.6.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0)) packages: - "@emnapi/core@1.9.2": - resolution: - { - integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==, - } - - "@emnapi/runtime@1.9.2": - resolution: - { - integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==, - } - - "@emnapi/wasi-threads@1.2.1": - resolution: - { - integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==, - } - - "@esbuild/aix-ppc64@0.27.7": - resolution: - { - integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==, - } - engines: { node: ">=18" } + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} cpu: [ppc64] os: [aix] - "@esbuild/android-arm64@0.27.7": - resolution: - { - integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==, - } - engines: { node: ">=18" } + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} cpu: [arm64] os: [android] - "@esbuild/android-arm@0.27.7": - resolution: - { - integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==, - } - engines: { node: ">=18" } + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} cpu: [arm] os: [android] - "@esbuild/android-x64@0.27.7": - resolution: - { - integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==, - } - engines: { node: ">=18" } + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} cpu: [x64] os: [android] - "@esbuild/darwin-arm64@0.27.7": - resolution: - { - integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==, - } - engines: { node: ">=18" } + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] - "@esbuild/darwin-x64@0.27.7": - resolution: - { - integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==, - } - engines: { node: ">=18" } + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] - "@esbuild/freebsd-arm64@0.27.7": - resolution: - { - integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==, - } - engines: { node: ">=18" } + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - "@esbuild/freebsd-x64@0.27.7": - resolution: - { - integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==, - } - engines: { node: ">=18" } + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} cpu: [x64] os: [freebsd] - "@esbuild/linux-arm64@0.27.7": - resolution: - { - integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==, - } - engines: { node: ">=18" } + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] - "@esbuild/linux-arm@0.27.7": - resolution: - { - integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==, - } - engines: { node: ">=18" } + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} cpu: [arm] os: [linux] - "@esbuild/linux-ia32@0.27.7": - resolution: - { - integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==, - } - engines: { node: ">=18" } + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} cpu: [ia32] os: [linux] - "@esbuild/linux-loong64@0.27.7": - resolution: - { - integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==, - } - engines: { node: ">=18" } + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} cpu: [loong64] os: [linux] - "@esbuild/linux-mips64el@0.27.7": - resolution: - { - integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==, - } - engines: { node: ">=18" } + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} cpu: [mips64el] os: [linux] - "@esbuild/linux-ppc64@0.27.7": - resolution: - { - integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==, - } - engines: { node: ">=18" } + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} cpu: [ppc64] os: [linux] - "@esbuild/linux-riscv64@0.27.7": - resolution: - { - integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==, - } - engines: { node: ">=18" } + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} cpu: [riscv64] os: [linux] - "@esbuild/linux-s390x@0.27.7": - resolution: - { - integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==, - } - engines: { node: ">=18" } + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} cpu: [s390x] os: [linux] - "@esbuild/linux-x64@0.27.7": - resolution: - { - integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==, - } - engines: { node: ">=18" } + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} cpu: [x64] os: [linux] - "@esbuild/netbsd-arm64@0.27.7": - resolution: - { - integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==, - } - engines: { node: ">=18" } + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - "@esbuild/netbsd-x64@0.27.7": - resolution: - { - integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==, - } - engines: { node: ">=18" } + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} cpu: [x64] os: [netbsd] - "@esbuild/openbsd-arm64@0.27.7": - resolution: - { - integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==, - } - engines: { node: ">=18" } + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - "@esbuild/openbsd-x64@0.27.7": - resolution: - { - integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==, - } - engines: { node: ">=18" } + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} cpu: [x64] os: [openbsd] - "@esbuild/openharmony-arm64@0.27.7": - resolution: - { - integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==, - } - engines: { node: ">=18" } + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - "@esbuild/sunos-x64@0.27.7": - resolution: - { - integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==, - } - engines: { node: ">=18" } + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} cpu: [x64] os: [sunos] - "@esbuild/win32-arm64@0.27.7": - resolution: - { - integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==, - } - engines: { node: ">=18" } + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} cpu: [arm64] os: [win32] - "@esbuild/win32-ia32@0.27.7": - resolution: - { - integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==, - } - engines: { node: ">=18" } + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} cpu: [ia32] os: [win32] - "@esbuild/win32-x64@0.27.7": - resolution: - { - integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==, - } - engines: { node: ">=18" } + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} cpu: [x64] os: [win32] - "@jridgewell/sourcemap-codec@1.5.5": - resolution: - { - integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, - } - - "@napi-rs/wasm-runtime@1.1.4": - resolution: - { - integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==, - } + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: - "@emnapi/core": ^1.7.1 - "@emnapi/runtime": ^1.7.1 - - "@noble/hashes@1.8.0": - resolution: - { - integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==, - } - engines: { node: ^14.21.3 || >=16 } - - "@oxc-project/types@0.126.0": - resolution: - { - integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==, - } - - "@paralleldrive/cuid2@2.3.1": - resolution: - { - integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==, - } - - "@rolldown/binding-android-arm64@1.0.0-rc.16": - resolution: - { - integrity: sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@oxc-project/types@0.126.0': + resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==} + + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + + '@rolldown/binding-android-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - "@rolldown/binding-darwin-arm64@1.0.0-rc.16": - resolution: - { - integrity: sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - "@rolldown/binding-darwin-x64@1.0.0-rc.16": - resolution: - { - integrity: sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + resolution: {integrity: sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - "@rolldown/binding-freebsd-x64@1.0.0-rc.16": - resolution: - { - integrity: sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + resolution: {integrity: sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - "@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16": - resolution: - { - integrity: sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + resolution: {integrity: sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - "@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16": - resolution: - { - integrity: sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - "@rolldown/binding-linux-arm64-musl@1.0.0-rc.16": - resolution: - { - integrity: sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - "@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16": - resolution: - { - integrity: sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - "@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16": - resolution: - { - integrity: sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - "@rolldown/binding-linux-x64-gnu@1.0.0-rc.16": - resolution: - { - integrity: sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - "@rolldown/binding-linux-x64-musl@1.0.0-rc.16": - resolution: - { - integrity: sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - "@rolldown/binding-openharmony-arm64@1.0.0-rc.16": - resolution: - { - integrity: sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - "@rolldown/binding-wasm32-wasi@1.0.0-rc.16": - resolution: - { - integrity: sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + resolution: {integrity: sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - "@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16": - resolution: - { - integrity: sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - "@rolldown/binding-win32-x64-msvc@1.0.0-rc.16": - resolution: - { - integrity: sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - "@rolldown/pluginutils@1.0.0-rc.16": - resolution: - { - integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==, - } - - "@standard-schema/spec@1.1.0": - resolution: - { - integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==, - } - - "@tybys/wasm-util@0.10.1": - resolution: - { - integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==, - } - - "@types/body-parser@1.19.6": - resolution: - { - integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==, - } - - "@types/chai@5.2.3": - resolution: - { - integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==, - } - - "@types/connect@3.4.38": - resolution: - { - integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==, - } - - "@types/cookie-parser@1.4.10": - resolution: - { - integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==, - } + '@rolldown/pluginutils@1.0.0-rc.16': + resolution: {integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/busboy@1.5.4': + resolution: {integrity: sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie-parser@1.4.10': + resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==} peerDependencies: - "@types/express": "*" - - "@types/cookiejar@2.1.5": - resolution: - { - integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==, - } - - "@types/cors@2.8.19": - resolution: - { - integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==, - } - - "@types/deep-eql@4.0.2": - resolution: - { - integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==, - } - - "@types/estree@1.0.8": - resolution: - { - integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, - } - - "@types/express-serve-static-core@5.1.1": - resolution: - { - integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==, - } - - "@types/express@5.0.6": - resolution: - { - integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==, - } - - "@types/http-errors@2.0.5": - resolution: - { - integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==, - } - - "@types/jsonwebtoken@9.0.10": - resolution: - { - integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==, - } - - "@types/methods@1.1.4": - resolution: - { - integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==, - } - - "@types/ms@2.1.0": - resolution: - { - integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==, - } - - "@types/node@25.6.0": - resolution: - { - integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==, - } - - "@types/pg@8.20.0": - resolution: - { - integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==, - } - - "@types/qs@6.15.0": - resolution: - { - integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==, - } - - "@types/range-parser@1.2.7": - resolution: - { - integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==, - } - - "@types/send@1.2.1": - resolution: - { - integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==, - } - - "@types/serve-static@2.2.0": - resolution: - { - integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==, - } - - "@types/superagent@8.1.9": - resolution: - { - integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==, - } - - "@types/supertest@7.2.0": - resolution: - { - integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==, - } - - "@vitest/expect@4.1.5": - resolution: - { - integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==, - } - - "@vitest/mocker@4.1.5": - resolution: - { - integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==, - } + '@types/express': '*' + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/supertest@7.2.0': + resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==} + + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -665,350 +457,212 @@ packages: vite: optional: true - "@vitest/pretty-format@4.1.5": - resolution: - { - integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==, - } - - "@vitest/runner@4.1.5": - resolution: - { - integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==, - } - - "@vitest/snapshot@4.1.5": - resolution: - { - integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==, - } - - "@vitest/spy@4.1.5": - resolution: - { - integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==, - } - - "@vitest/utils@4.1.5": - resolution: - { - integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==, - } + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} accepts@2.0.0: - resolution: - { - integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} asap@2.0.6: - resolution: - { - integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==, - } + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} assertion-error@2.0.1: - resolution: - { - integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} asynckit@0.4.0: - resolution: - { - integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, - } + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} body-parser@2.2.2: - resolution: - { - integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} buffer-equal-constant-time@1.0.1: - resolution: - { - integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==, - } + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} bytes@3.1.2: - resolution: - { - integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} call-bind-apply-helpers@1.0.2: - resolution: - { - integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} call-bound@1.0.4: - resolution: - { - integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} chai@6.2.2: - resolution: - { - integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} combined-stream@1.0.8: - resolution: - { - integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} component-emitter@1.3.1: - resolution: - { - integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==, - } + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} content-disposition@1.1.0: - resolution: - { - integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} content-type@1.0.5: - resolution: - { - integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} convert-source-map@2.0.0: - resolution: - { - integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, - } + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} cookie-parser@1.4.7: - resolution: - { - integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==, - } - engines: { node: ">= 0.8.0" } + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} cookie-signature@1.0.6: - resolution: - { - integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==, - } + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} cookie-signature@1.2.2: - resolution: - { - integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==, - } - engines: { node: ">=6.6.0" } + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} cookie@0.7.2: - resolution: - { - integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} cookiejar@2.1.4: - resolution: - { - integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==, - } + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} cors@2.8.6: - resolution: - { - integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==, - } - engines: { node: ">= 0.10" } + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + csv-parse@6.2.1: + resolution: {integrity: sha512-LRLMV+UCyfMokp8Wb411duBf1gaBKJfOfBWU9eHMJ+b+cJYZsNu3AFmjJf3+yPGd59Exz1TsMjaSFyxnYB9+IQ==} csv-stringify@6.7.0: - resolution: - { - integrity: sha512-UdtziYp5HuTz7e5j8Nvq+a/3HQo+2/aJZ9xntNTpmRRIg/3YYqDVgiS9fvAhtNbnyfbv2ZBe0bqCHqzhE7FqWQ==, - } + resolution: {integrity: sha512-UdtziYp5HuTz7e5j8Nvq+a/3HQo+2/aJZ9xntNTpmRRIg/3YYqDVgiS9fvAhtNbnyfbv2ZBe0bqCHqzhE7FqWQ==} debug@4.4.3: - resolution: - { - integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, - } - engines: { node: ">=6.0" } + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} peerDependencies: - supports-color: "*" + supports-color: '*' peerDependenciesMeta: supports-color: optional: true delayed-stream@1.0.0: - resolution: - { - integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, - } - engines: { node: ">=0.4.0" } + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} depd@2.0.0: - resolution: - { - integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} detect-libc@2.1.2: - resolution: - { - integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} dezalgo@1.0.4: - resolution: - { - integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==, - } + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} dotenv@17.4.2: - resolution: - { - integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} dunder-proto@1.0.1: - resolution: - { - integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} ecdsa-sig-formatter@1.0.11: - resolution: - { - integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==, - } + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} ee-first@1.1.1: - resolution: - { - integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, - } + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} encodeurl@2.0.0: - resolution: - { - integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} es-define-property@1.0.1: - resolution: - { - integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} es-errors@1.3.0: - resolution: - { - integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} es-module-lexer@2.0.0: - resolution: - { - integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==, - } + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} es-object-atoms@1.1.1: - resolution: - { - integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} es-set-tostringtag@2.1.0: - resolution: - { - integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} esbuild@0.27.7: - resolution: - { - integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} hasBin: true escape-html@1.0.3: - resolution: - { - integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, - } + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} estree-walker@3.0.3: - resolution: - { - integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, - } + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} etag@1.8.1: - resolution: - { - integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} expect-type@1.3.0: - resolution: - { - integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==, - } - engines: { node: ">=12.0.0" } + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} express-rate-limit@8.4.1: - resolution: - { - integrity: sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==, - } - engines: { node: ">= 16" } + resolution: {integrity: sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==} + engines: {node: '>= 16'} peerDependencies: - express: ">= 4.11" + express: '>= 4.11' express@5.2.1: - resolution: - { - integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==, - } - engines: { node: ">= 18" } + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} fast-safe-stringify@2.1.1: - resolution: - { - integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==, - } + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} fdir@6.5.0: - resolution: - { - integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, - } - engines: { node: ">=12.0.0" } + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -1016,850 +670,521 @@ packages: optional: true finalhandler@2.1.1: - resolution: - { - integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==, - } - engines: { node: ">= 18.0.0" } + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} form-data@4.0.5: - resolution: - { - integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==, - } - engines: { node: ">= 6" } + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} formidable@3.5.4: - resolution: - { - integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==, - } - engines: { node: ">=14.0.0" } + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} forwarded@0.2.0: - resolution: - { - integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} fresh@2.0.0: - resolution: - { - integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} fsevents@2.3.3: - resolution: - { - integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, - } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] function-bind@1.1.2: - resolution: - { - integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, - } + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} get-intrinsic@1.3.0: - resolution: - { - integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} get-proto@1.0.1: - resolution: - { - integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} get-tsconfig@4.14.0: - resolution: - { - integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==, - } + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} gopd@1.2.0: - resolution: - { - integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} has-symbols@1.1.0: - resolution: - { - integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} has-tostringtag@1.0.2: - resolution: - { - integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} hasown@2.0.2: - resolution: - { - integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} http-errors@2.0.1: - resolution: - { - integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} iconv-lite@0.7.2: - resolution: - { - integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} inherits@2.0.4: - resolution: - { - integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, - } + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ioredis@5.10.1: + resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} + engines: {node: '>=12.22.0'} ip-address@10.1.0: - resolution: - { - integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==, - } - engines: { node: ">= 12" } + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} ipaddr.js@1.9.1: - resolution: - { - integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==, - } - engines: { node: ">= 0.10" } + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} is-promise@4.0.0: - resolution: - { - integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==, - } + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} jsonwebtoken@9.0.3: - resolution: - { - integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==, - } - engines: { node: ">=12", npm: ">=6" } + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} jwa@2.0.1: - resolution: - { - integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==, - } + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} jws@4.0.1: - resolution: - { - integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==, - } + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} lightningcss-android-arm64@1.32.0: - resolution: - { - integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] lightningcss-darwin-arm64@1.32.0: - resolution: - { - integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] lightningcss-darwin-x64@1.32.0: - resolution: - { - integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] lightningcss-freebsd-x64@1.32.0: - resolution: - { - integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: - { - integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] lightningcss-linux-arm64-gnu@1.32.0: - resolution: - { - integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] lightningcss-linux-arm64-musl@1.32.0: - resolution: - { - integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] lightningcss-linux-x64-gnu@1.32.0: - resolution: - { - integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] lightningcss-linux-x64-musl@1.32.0: - resolution: - { - integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] lightningcss-win32-arm64-msvc@1.32.0: - resolution: - { - integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] lightningcss-win32-x64-msvc@1.32.0: - resolution: - { - integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] lightningcss@1.32.0: - resolution: - { - integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==, - } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} lodash.includes@4.3.0: - resolution: - { - integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==, - } + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} lodash.isboolean@3.0.3: - resolution: - { - integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==, - } + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} lodash.isinteger@4.0.4: - resolution: - { - integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==, - } + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} lodash.isnumber@3.0.3: - resolution: - { - integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==, - } + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} lodash.isplainobject@4.0.6: - resolution: - { - integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==, - } + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} lodash.isstring@4.0.1: - resolution: - { - integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==, - } + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} lodash.once@4.1.1: - resolution: - { - integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==, - } + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} magic-string@0.30.21: - resolution: - { - integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, - } + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} math-intrinsics@1.1.0: - resolution: - { - integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} media-typer@1.1.0: - resolution: - { - integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} merge-descriptors@2.0.0: - resolution: - { - integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} methods@1.1.2: - resolution: - { - integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} mime-db@1.52.0: - resolution: - { - integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} mime-db@1.54.0: - resolution: - { - integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} mime-types@2.1.35: - resolution: - { - integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} mime-types@3.0.2: - resolution: - { - integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} mime@2.6.0: - resolution: - { - integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==, - } - engines: { node: ">=4.0.0" } + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} hasBin: true ms@2.1.3: - resolution: - { - integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, - } + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} nanoid@3.3.11: - resolution: - { - integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, - } - engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true negotiator@1.0.0: - resolution: - { - integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} object-assign@4.1.1: - resolution: - { - integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} object-inspect@1.13.4: - resolution: - { - integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} obug@2.1.1: - resolution: - { - integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==, - } + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} on-finished@2.4.1: - resolution: - { - integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} once@1.4.0: - resolution: - { - integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, - } + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} parseurl@1.3.3: - resolution: - { - integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} path-to-regexp@8.4.2: - resolution: - { - integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==, - } + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} pathe@2.0.3: - resolution: - { - integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, - } + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} pg-cloudflare@1.3.0: - resolution: - { - integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==, - } + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} pg-connection-string@2.12.0: - resolution: - { - integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==, - } + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} pg-int8@1.0.1: - resolution: - { - integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==, - } - engines: { node: ">=4.0.0" } + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} pg-pool@3.13.0: - resolution: - { - integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==, - } + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} peerDependencies: - pg: ">=8.0" + pg: '>=8.0' pg-protocol@1.13.0: - resolution: - { - integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==, - } + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} pg-types@2.2.0: - resolution: - { - integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} pg@8.20.0: - resolution: - { - integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==, - } - engines: { node: ">= 16.0.0" } + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} peerDependencies: - pg-native: ">=3.0.1" + pg-native: '>=3.0.1' peerDependenciesMeta: pg-native: optional: true pgpass@1.0.5: - resolution: - { - integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==, - } + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} picocolors@1.1.1: - resolution: - { - integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, - } + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@4.0.4: - resolution: - { - integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} postcss@8.5.10: - resolution: - { - integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==, - } - engines: { node: ^10 || ^12 || >=14 } + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: - resolution: - { - integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} postgres-bytea@1.0.1: - resolution: - { - integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} postgres-date@1.0.7: - resolution: - { - integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} postgres-interval@1.2.0: - resolution: - { - integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} prettier@3.8.3: - resolution: - { - integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==, - } - engines: { node: ">=14" } + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} hasBin: true proxy-addr@2.0.7: - resolution: - { - integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==, - } - engines: { node: ">= 0.10" } + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} qs@6.15.1: - resolution: - { - integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==, - } - engines: { node: ">=0.6" } + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} range-parser@1.2.1: - resolution: - { - integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} raw-body@3.0.2: - resolution: - { - integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==, - } - engines: { node: ">= 0.10" } + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} resolve-pkg-maps@1.0.0: - resolution: - { - integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, - } + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} rolldown@1.0.0-rc.16: - resolution: - { - integrity: sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + resolution: {integrity: sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true router@2.2.0: - resolution: - { - integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==, - } - engines: { node: ">= 18" } + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} safe-buffer@5.2.1: - resolution: - { - integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, - } + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} safer-buffer@2.1.2: - resolution: - { - integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, - } + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} semver@7.7.4: - resolution: - { - integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} hasBin: true send@1.2.1: - resolution: - { - integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==, - } - engines: { node: ">= 18" } + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} serve-static@2.2.1: - resolution: - { - integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==, - } - engines: { node: ">= 18" } + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} setprototypeof@1.2.0: - resolution: - { - integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, - } + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} side-channel-list@1.0.1: - resolution: - { - integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} side-channel-map@1.0.1: - resolution: - { - integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} side-channel-weakmap@1.0.2: - resolution: - { - integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} side-channel@1.1.0: - resolution: - { - integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==, - } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} siginfo@2.0.0: - resolution: - { - integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==, - } + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} source-map-js@1.2.1: - resolution: - { - integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} split2@4.2.0: - resolution: - { - integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==, - } - engines: { node: ">= 10.x" } + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} stackback@0.0.2: - resolution: - { - integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, - } + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} statuses@2.0.2: - resolution: - { - integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} std-env@4.1.0: - resolution: - { - integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==, - } + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} superagent@10.3.0: - resolution: - { - integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==, - } - engines: { node: ">=14.18.0" } + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} supertest@7.2.2: - resolution: - { - integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==, - } - engines: { node: ">=14.18.0" } + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + engines: {node: '>=14.18.0'} tinybench@2.9.0: - resolution: - { - integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==, - } + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinyexec@1.1.1: - resolution: - { - integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} tinyglobby@0.2.16: - resolution: - { - integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==, - } - engines: { node: ">=12.0.0" } + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} tinyrainbow@3.1.0: - resolution: - { - integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==, - } - engines: { node: ">=14.0.0" } + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} toidentifier@1.0.1: - resolution: - { - integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==, - } - engines: { node: ">=0.6" } + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} tslib@2.8.1: - resolution: - { - integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, - } + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} tsx@4.21.0: - resolution: - { - integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==, - } - engines: { node: ">=18.0.0" } + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} hasBin: true type-is@2.0.1: - resolution: - { - integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==, - } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} typescript@6.0.2: - resolution: - { - integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==, - } - engines: { node: ">=14.17" } + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} + engines: {node: '>=14.17'} hasBin: true undici-types@7.19.2: - resolution: - { - integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==, - } + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} unpipe@1.0.0: - resolution: - { - integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} uuid@13.0.0: - resolution: - { - integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==, - } + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true vary@1.1.2: - resolution: - { - integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, - } - engines: { node: ">= 0.8" } + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} vite@8.0.9: - resolution: - { - integrity: sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==, - } - engines: { node: ^20.19.0 || >=22.12.0 } + resolution: {integrity: sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - "@types/node": ^20.19.0 || >=22.12.0 - "@vitejs/devtools": ^0.1.0 + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 esbuild: ^0.27.0 || ^0.28.0 - jiti: ">=1.21.0" + jiti: '>=1.21.0' less: ^4.0.0 sass: ^1.70.0 sass-embedded: ^1.70.0 - stylus: ">=0.54.8" + stylus: '>=0.54.8' sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: - "@types/node": + '@types/node': optional: true - "@vitejs/devtools": + '@vitejs/devtools': optional: true esbuild: optional: true @@ -1883,43 +1208,40 @@ packages: optional: true vitest@4.1.5: - resolution: - { - integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==, - } - engines: { node: ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: - "@edge-runtime/vm": "*" - "@opentelemetry/api": ^1.9.0 - "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.1.5 - "@vitest/browser-preview": 4.1.5 - "@vitest/browser-webdriverio": 4.1.5 - "@vitest/coverage-istanbul": 4.1.5 - "@vitest/coverage-v8": 4.1.5 - "@vitest/ui": 4.1.5 - happy-dom: "*" - jsdom: "*" + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 + happy-dom: '*' + jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: - "@edge-runtime/vm": + '@edge-runtime/vm': optional: true - "@opentelemetry/api": + '@opentelemetry/api': optional: true - "@types/node": + '@types/node': optional: true - "@vitest/browser-playwright": + '@vitest/browser-playwright': optional: true - "@vitest/browser-preview": + '@vitest/browser-preview': optional: true - "@vitest/browser-webdriverio": + '@vitest/browser-webdriverio': optional: true - "@vitest/coverage-istanbul": + '@vitest/coverage-istanbul': optional: true - "@vitest/coverage-v8": + '@vitest/coverage-v8': optional: true - "@vitest/ui": + '@vitest/ui': optional: true happy-dom: optional: true @@ -1927,321 +1249,319 @@ packages: optional: true why-is-node-running@2.3.0: - resolution: - { - integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} hasBin: true wrappy@1.0.2: - resolution: - { - integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, - } + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} xtend@4.0.2: - resolution: - { - integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==, - } - engines: { node: ">=0.4" } + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} snapshots: - "@emnapi/core@1.9.2": + + '@emnapi/core@1.9.2': dependencies: - "@emnapi/wasi-threads": 1.2.1 + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - "@emnapi/runtime@1.9.2": + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 optional: true - "@emnapi/wasi-threads@1.2.1": + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true - "@esbuild/aix-ppc64@0.27.7": + '@esbuild/aix-ppc64@0.27.7': optional: true - "@esbuild/android-arm64@0.27.7": + '@esbuild/android-arm64@0.27.7': optional: true - "@esbuild/android-arm@0.27.7": + '@esbuild/android-arm@0.27.7': optional: true - "@esbuild/android-x64@0.27.7": + '@esbuild/android-x64@0.27.7': optional: true - "@esbuild/darwin-arm64@0.27.7": + '@esbuild/darwin-arm64@0.27.7': optional: true - "@esbuild/darwin-x64@0.27.7": + '@esbuild/darwin-x64@0.27.7': optional: true - "@esbuild/freebsd-arm64@0.27.7": + '@esbuild/freebsd-arm64@0.27.7': optional: true - "@esbuild/freebsd-x64@0.27.7": + '@esbuild/freebsd-x64@0.27.7': optional: true - "@esbuild/linux-arm64@0.27.7": + '@esbuild/linux-arm64@0.27.7': optional: true - "@esbuild/linux-arm@0.27.7": + '@esbuild/linux-arm@0.27.7': optional: true - "@esbuild/linux-ia32@0.27.7": + '@esbuild/linux-ia32@0.27.7': optional: true - "@esbuild/linux-loong64@0.27.7": + '@esbuild/linux-loong64@0.27.7': optional: true - "@esbuild/linux-mips64el@0.27.7": + '@esbuild/linux-mips64el@0.27.7': optional: true - "@esbuild/linux-ppc64@0.27.7": + '@esbuild/linux-ppc64@0.27.7': optional: true - "@esbuild/linux-riscv64@0.27.7": + '@esbuild/linux-riscv64@0.27.7': optional: true - "@esbuild/linux-s390x@0.27.7": + '@esbuild/linux-s390x@0.27.7': optional: true - "@esbuild/linux-x64@0.27.7": + '@esbuild/linux-x64@0.27.7': optional: true - "@esbuild/netbsd-arm64@0.27.7": + '@esbuild/netbsd-arm64@0.27.7': optional: true - "@esbuild/netbsd-x64@0.27.7": + '@esbuild/netbsd-x64@0.27.7': optional: true - "@esbuild/openbsd-arm64@0.27.7": + '@esbuild/openbsd-arm64@0.27.7': optional: true - "@esbuild/openbsd-x64@0.27.7": + '@esbuild/openbsd-x64@0.27.7': optional: true - "@esbuild/openharmony-arm64@0.27.7": + '@esbuild/openharmony-arm64@0.27.7': optional: true - "@esbuild/sunos-x64@0.27.7": + '@esbuild/sunos-x64@0.27.7': optional: true - "@esbuild/win32-arm64@0.27.7": + '@esbuild/win32-arm64@0.27.7': optional: true - "@esbuild/win32-ia32@0.27.7": + '@esbuild/win32-ia32@0.27.7': optional: true - "@esbuild/win32-x64@0.27.7": + '@esbuild/win32-x64@0.27.7': optional: true - "@jridgewell/sourcemap-codec@1.5.5": {} + '@ioredis/commands@1.5.1': {} - "@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)": + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - "@emnapi/core": 1.9.2 - "@emnapi/runtime": 1.9.2 - "@tybys/wasm-util": 0.10.1 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 optional: true - "@noble/hashes@1.8.0": {} + '@noble/hashes@1.8.0': {} - "@oxc-project/types@0.126.0": {} + '@oxc-project/types@0.126.0': {} - "@paralleldrive/cuid2@2.3.1": + '@paralleldrive/cuid2@2.3.1': dependencies: - "@noble/hashes": 1.8.0 + '@noble/hashes': 1.8.0 - "@rolldown/binding-android-arm64@1.0.0-rc.16": + '@rolldown/binding-android-arm64@1.0.0-rc.16': optional: true - "@rolldown/binding-darwin-arm64@1.0.0-rc.16": + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': optional: true - "@rolldown/binding-darwin-x64@1.0.0-rc.16": + '@rolldown/binding-darwin-x64@1.0.0-rc.16': optional: true - "@rolldown/binding-freebsd-x64@1.0.0-rc.16": + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': optional: true - "@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16": + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': optional: true - "@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16": + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': optional: true - "@rolldown/binding-linux-arm64-musl@1.0.0-rc.16": + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': optional: true - "@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16": + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': optional: true - "@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16": + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': optional: true - "@rolldown/binding-linux-x64-gnu@1.0.0-rc.16": + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': optional: true - "@rolldown/binding-linux-x64-musl@1.0.0-rc.16": + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': optional: true - "@rolldown/binding-openharmony-arm64@1.0.0-rc.16": + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': optional: true - "@rolldown/binding-wasm32-wasi@1.0.0-rc.16": + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': dependencies: - "@emnapi/core": 1.9.2 - "@emnapi/runtime": 1.9.2 - "@napi-rs/wasm-runtime": 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true - "@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16": + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': optional: true - "@rolldown/binding-win32-x64-msvc@1.0.0-rc.16": + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': optional: true - "@rolldown/pluginutils@1.0.0-rc.16": {} + '@rolldown/pluginutils@1.0.0-rc.16': {} - "@standard-schema/spec@1.1.0": {} + '@standard-schema/spec@1.1.0': {} - "@tybys/wasm-util@0.10.1": + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true - "@types/body-parser@1.19.6": + '@types/body-parser@1.19.6': dependencies: - "@types/connect": 3.4.38 - "@types/node": 25.6.0 + '@types/connect': 3.4.38 + '@types/node': 25.6.0 - "@types/chai@5.2.3": + '@types/busboy@1.5.4': dependencies: - "@types/deep-eql": 4.0.2 + '@types/node': 25.6.0 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - "@types/connect@3.4.38": + '@types/connect@3.4.38': dependencies: - "@types/node": 25.6.0 + '@types/node': 25.6.0 - "@types/cookie-parser@1.4.10(@types/express@5.0.6)": + '@types/cookie-parser@1.4.10(@types/express@5.0.6)': dependencies: - "@types/express": 5.0.6 + '@types/express': 5.0.6 - "@types/cookiejar@2.1.5": {} + '@types/cookiejar@2.1.5': {} - "@types/cors@2.8.19": + '@types/cors@2.8.19': dependencies: - "@types/node": 25.6.0 + '@types/node': 25.6.0 - "@types/deep-eql@4.0.2": {} + '@types/deep-eql@4.0.2': {} - "@types/estree@1.0.8": {} + '@types/estree@1.0.8': {} - "@types/express-serve-static-core@5.1.1": + '@types/express-serve-static-core@5.1.1': dependencies: - "@types/node": 25.6.0 - "@types/qs": 6.15.0 - "@types/range-parser": 1.2.7 - "@types/send": 1.2.1 + '@types/node': 25.6.0 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 - "@types/express@5.0.6": + '@types/express@5.0.6': dependencies: - "@types/body-parser": 1.19.6 - "@types/express-serve-static-core": 5.1.1 - "@types/serve-static": 2.2.0 + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 - "@types/http-errors@2.0.5": {} + '@types/http-errors@2.0.5': {} - "@types/jsonwebtoken@9.0.10": + '@types/jsonwebtoken@9.0.10': dependencies: - "@types/ms": 2.1.0 - "@types/node": 25.6.0 + '@types/ms': 2.1.0 + '@types/node': 25.6.0 - "@types/methods@1.1.4": {} + '@types/methods@1.1.4': {} - "@types/ms@2.1.0": {} + '@types/ms@2.1.0': {} - "@types/node@25.6.0": + '@types/node@25.6.0': dependencies: undici-types: 7.19.2 - "@types/pg@8.20.0": + '@types/pg@8.20.0': dependencies: - "@types/node": 25.6.0 + '@types/node': 25.6.0 pg-protocol: 1.13.0 pg-types: 2.2.0 - "@types/qs@6.15.0": {} + '@types/qs@6.15.0': {} - "@types/range-parser@1.2.7": {} + '@types/range-parser@1.2.7': {} - "@types/send@1.2.1": + '@types/send@1.2.1': dependencies: - "@types/node": 25.6.0 + '@types/node': 25.6.0 - "@types/serve-static@2.2.0": + '@types/serve-static@2.2.0': dependencies: - "@types/http-errors": 2.0.5 - "@types/node": 25.6.0 + '@types/http-errors': 2.0.5 + '@types/node': 25.6.0 - "@types/superagent@8.1.9": + '@types/superagent@8.1.9': dependencies: - "@types/cookiejar": 2.1.5 - "@types/methods": 1.1.4 - "@types/node": 25.6.0 + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 25.6.0 form-data: 4.0.5 - "@types/supertest@7.2.0": + '@types/supertest@7.2.0': dependencies: - "@types/methods": 1.1.4 - "@types/superagent": 8.1.9 + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 - "@vitest/expect@4.1.5": + '@vitest/expect@4.1.5': dependencies: - "@standard-schema/spec": 1.1.0 - "@types/chai": 5.2.3 - "@vitest/spy": 4.1.5 - "@vitest/utils": 4.1.5 + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 chai: 6.2.2 tinyrainbow: 3.1.0 - "@vitest/mocker@4.1.5(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0))": + '@vitest/mocker@4.1.5(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0))': dependencies: - "@vitest/spy": 4.1.5 + '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0) - "@vitest/pretty-format@4.1.5": + '@vitest/pretty-format@4.1.5': dependencies: tinyrainbow: 3.1.0 - "@vitest/runner@4.1.5": + '@vitest/runner@4.1.5': dependencies: - "@vitest/utils": 4.1.5 + '@vitest/utils': 4.1.5 pathe: 2.0.3 - "@vitest/snapshot@4.1.5": + '@vitest/snapshot@4.1.5': dependencies: - "@vitest/pretty-format": 4.1.5 - "@vitest/utils": 4.1.5 + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 magic-string: 0.30.21 pathe: 2.0.3 - "@vitest/spy@4.1.5": {} + '@vitest/spy@4.1.5': {} - "@vitest/utils@4.1.5": + '@vitest/utils@4.1.5': dependencies: - "@vitest/pretty-format": 4.1.5 + '@vitest/pretty-format': 4.1.5 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -2272,6 +1592,10 @@ snapshots: buffer-equal-constant-time@1.0.1: {} + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -2286,6 +1610,8 @@ snapshots: chai@6.2.2: {} + cluster-key-slot@1.1.2: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -2316,6 +1642,8 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + csv-parse@6.2.1: {} + csv-stringify@6.7.0: {} debug@4.4.3: @@ -2324,6 +1652,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} detect-libc@2.1.2: {} @@ -2368,38 +1698,38 @@ snapshots: esbuild@0.27.7: optionalDependencies: - "@esbuild/aix-ppc64": 0.27.7 - "@esbuild/android-arm": 0.27.7 - "@esbuild/android-arm64": 0.27.7 - "@esbuild/android-x64": 0.27.7 - "@esbuild/darwin-arm64": 0.27.7 - "@esbuild/darwin-x64": 0.27.7 - "@esbuild/freebsd-arm64": 0.27.7 - "@esbuild/freebsd-x64": 0.27.7 - "@esbuild/linux-arm": 0.27.7 - "@esbuild/linux-arm64": 0.27.7 - "@esbuild/linux-ia32": 0.27.7 - "@esbuild/linux-loong64": 0.27.7 - "@esbuild/linux-mips64el": 0.27.7 - "@esbuild/linux-ppc64": 0.27.7 - "@esbuild/linux-riscv64": 0.27.7 - "@esbuild/linux-s390x": 0.27.7 - "@esbuild/linux-x64": 0.27.7 - "@esbuild/netbsd-arm64": 0.27.7 - "@esbuild/netbsd-x64": 0.27.7 - "@esbuild/openbsd-arm64": 0.27.7 - "@esbuild/openbsd-x64": 0.27.7 - "@esbuild/openharmony-arm64": 0.27.7 - "@esbuild/sunos-x64": 0.27.7 - "@esbuild/win32-arm64": 0.27.7 - "@esbuild/win32-ia32": 0.27.7 - "@esbuild/win32-x64": 0.27.7 + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 escape-html@1.0.3: {} estree-walker@3.0.3: dependencies: - "@types/estree": 1.0.8 + '@types/estree': 1.0.8 etag@1.8.1: {} @@ -2470,7 +1800,7 @@ snapshots: formidable@3.5.4: dependencies: - "@paralleldrive/cuid2": 2.3.1 + '@paralleldrive/cuid2': 2.3.1 dezalgo: 1.0.4 once: 1.4.0 @@ -2531,6 +1861,20 @@ snapshots: inherits@2.0.4: {} + ioredis@5.10.1: + dependencies: + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-address@10.1.0: {} ipaddr.js@1.9.1: {} @@ -2610,8 +1954,12 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lodash.defaults@4.2.0: {} + lodash.includes@4.3.0: {} + lodash.isarguments@3.1.0: {} + lodash.isboolean@3.0.3: {} lodash.isinteger@4.0.4: {} @@ -2626,7 +1974,7 @@ snapshots: magic-string@0.30.21: dependencies: - "@jridgewell/sourcemap-codec": 1.5.5 + '@jridgewell/sourcemap-codec': 1.5.5 math-intrinsics@1.1.0: {} @@ -2751,28 +2099,34 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + resolve-pkg-maps@1.0.0: {} rolldown@1.0.0-rc.16: dependencies: - "@oxc-project/types": 0.126.0 - "@rolldown/pluginutils": 1.0.0-rc.16 + '@oxc-project/types': 0.126.0 + '@rolldown/pluginutils': 1.0.0-rc.16 optionalDependencies: - "@rolldown/binding-android-arm64": 1.0.0-rc.16 - "@rolldown/binding-darwin-arm64": 1.0.0-rc.16 - "@rolldown/binding-darwin-x64": 1.0.0-rc.16 - "@rolldown/binding-freebsd-x64": 1.0.0-rc.16 - "@rolldown/binding-linux-arm-gnueabihf": 1.0.0-rc.16 - "@rolldown/binding-linux-arm64-gnu": 1.0.0-rc.16 - "@rolldown/binding-linux-arm64-musl": 1.0.0-rc.16 - "@rolldown/binding-linux-ppc64-gnu": 1.0.0-rc.16 - "@rolldown/binding-linux-s390x-gnu": 1.0.0-rc.16 - "@rolldown/binding-linux-x64-gnu": 1.0.0-rc.16 - "@rolldown/binding-linux-x64-musl": 1.0.0-rc.16 - "@rolldown/binding-openharmony-arm64": 1.0.0-rc.16 - "@rolldown/binding-wasm32-wasi": 1.0.0-rc.16 - "@rolldown/binding-win32-arm64-msvc": 1.0.0-rc.16 - "@rolldown/binding-win32-x64-msvc": 1.0.0-rc.16 + '@rolldown/binding-android-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-x64': 1.0.0-rc.16 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.16 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.16 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.16 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.16 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.16 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.16 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.16 router@2.2.0: dependencies: @@ -2853,10 +2207,14 @@ snapshots: stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + statuses@2.0.2: {} std-env@4.1.0: {} + streamsearch@1.1.0: {} + superagent@10.3.0: dependencies: component-emitter: 1.3.1 @@ -2926,20 +2284,20 @@ snapshots: rolldown: 1.0.0-rc.16 tinyglobby: 0.2.16 optionalDependencies: - "@types/node": 25.6.0 + '@types/node': 25.6.0 esbuild: 0.27.7 fsevents: 2.3.3 tsx: 4.21.0 vitest@4.1.5(@types/node@25.6.0)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0)): dependencies: - "@vitest/expect": 4.1.5 - "@vitest/mocker": 4.1.5(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0)) - "@vitest/pretty-format": 4.1.5 - "@vitest/runner": 4.1.5 - "@vitest/snapshot": 4.1.5 - "@vitest/spy": 4.1.5 - "@vitest/utils": 4.1.5 + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -2954,7 +2312,7 @@ snapshots: vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: - "@types/node": 25.6.0 + '@types/node': 25.6.0 transitivePeerDependencies: - msw diff --git a/scripts/generate-tokens.ts b/scripts/generate-tokens.ts index 0ec520d..9ff5608 100644 --- a/scripts/generate-tokens.ts +++ b/scripts/generate-tokens.ts @@ -1,62 +1,71 @@ // scripts/generate-tokens.ts -import jwt from 'jsonwebtoken'; -import crypto from 'crypto'; -import { config } from 'dotenv'; -import { DatabaseClient } from '../src/db'; -import * as uuid from 'uuid'; +import jwt from "jsonwebtoken"; +import crypto from "crypto"; +import { config } from "dotenv"; +import { DatabaseClient } from "../src/db"; +import * as uuid from "uuid"; config(); const db = new DatabaseClient(); const jwtSecret = process.env.JWT_SECRET!; -const adminId = '8889143d-9063-4994-a622-2b857591b3c4'; -const analystId = 'bcdc0edd-4a48-440d-96c7-b54289410c85'; +const adminId = "8889143d-9063-4994-a622-2b857591b3c4"; +const analystId = "bcdc0edd-4a48-440d-96c7-b54289410c85"; + +const localAdminId = "4c5357b0-c1e9-4049-85b7-1c8078883fd5"; +const localAnalystId = "5ccb602f-1a8d-4948-8b40-51fd4175ed25" async function generateToken() { - // Get the seeded users - const admin = await db.getUserById(adminId); - const analyst = await db.getUserById(analystId); - - // Generate long-lived tokens for the grader (use a long expiry) - const adminToken = jwt.sign( - { userId: admin?.id, role: 'admin' }, - jwtSecret, - { expiresIn: '30d' } - ); - - const analystToken = jwt.sign( - { userId: analyst?.id, role: 'analyst' }, - jwtSecret, - { expiresIn: '30d' } - ); - - // Generate and store refresh token for admin - const rawRefresh = crypto.randomBytes(32).toString('hex'); - const analystRefresh = crypto.randomBytes(32).toString('hex'); - const tokenHash = crypto.createHash('sha256').update(rawRefresh).digest('hex'); - const analystTokenHash = crypto.createHash('sha256').update(analystRefresh).digest('hex'); - await db.createSession({ - id: uuid.v7(), - user_id: admin?.id ?? '8889143d-9063-4994-a622-2b857591b3c4', - token_hash: tokenHash, - expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), - }); - - await db.createSession({ - id: uuid.v7(), - user_id: analyst?.id ?? 'bcdc0edd-4a48-440d-96c7-b54289410c85', - token_hash: analystTokenHash, - expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), - }); - - console.log('Admin token:', adminToken); - console.log('Analyst token:', analystToken); - console.log('Refresh token (admin):', rawRefresh); - console.log('Refresh token (analyst): ', analystRefresh); + // Get the seeded users + const isLocal = true; + + const admin = await db.getUserById(isLocal ? localAdminId :adminId); + const analyst = await db.getUserById(isLocal ? localAnalystId :analystId); + + // Generate long-lived tokens for the grader (use a long expiry) + const adminToken = jwt.sign({ userId: admin?.id, role: "admin" }, jwtSecret, { + expiresIn: "30d", + }); + + const analystToken = jwt.sign( + { userId: analyst?.id, role: "analyst" }, + jwtSecret, + { expiresIn: "30d" }, + ); + + // Generate and store refresh token for admin + const rawRefresh = crypto.randomBytes(32).toString("hex"); + const analystRefresh = crypto.randomBytes(32).toString("hex"); + const tokenHash = crypto + .createHash("sha256") + .update(rawRefresh) + .digest("hex"); + const analystTokenHash = crypto + .createHash("sha256") + .update(analystRefresh) + .digest("hex"); + await db.createSession({ + id: uuid.v7(), + user_id: admin?.id ?? isLocal ? localAdminId: "8889143d-9063-4994-a622-2b857591b3c4", + token_hash: tokenHash, + expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + }); + + await db.createSession({ + id: uuid.v7(), + user_id: analyst?.id ?? isLocal ? localAnalystId : "bcdc0edd-4a48-440d-96c7-b54289410c85", + token_hash: analystTokenHash, + expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + }); + + console.log("Admin token:", adminToken); + console.log("Analyst token:", analystToken); + console.log("Refresh token (admin):", rawRefresh); + console.log("Refresh token (analyst): ", analystRefresh); } generateToken().catch((err) => { - console.error("Gen token failed:", err); - process.exit(1); -}) \ No newline at end of file + console.error("Gen token failed:", err); + process.exit(1); +}); diff --git a/scripts/generate_csv.py b/scripts/generate_csv.py new file mode 100644 index 0000000..119d58a --- /dev/null +++ b/scripts/generate_csv.py @@ -0,0 +1,130 @@ +""" +Generate a CSV file with 500,000 random profile rows for testing CSV ingestion. +Each run produces different names using UUID-based suffixes. + +Usage: + python scripts/generate_csv.py + python scripts/generate_csv.py --rows 100000 --output test_small.csv +""" + +import csv +import random +import uuid +import argparse +from pathlib import Path + +GENDERS = ["male", "female"] + +COUNTRIES = [ + ("NG", "Nigeria"), + ("KE", "Kenya"), + ("GH", "Ghana"), + ("ZA", "South Africa"), + ("EG", "Egypt"), + ("US", "United States"), + ("GB", "United Kingdom"), + ("DE", "Germany"), + ("FR", "France"), + ("IN", "India"), + ("BR", "Brazil"), + ("MX", "Mexico"), + ("JP", "Japan"), + ("CN", "China"), + ("AU", "Australia"), + ("CA", "Canada"), + ("IT", "Italy"), + ("ES", "Spain"), + ("PH", "Philippines"), + ("PK", "Pakistan"), +] + +FIRST_NAMES = [ + "Amara", "Chidi", "Fatima", "Kwame", "Ngozi", "Emeka", "Aisha", "Kofi", + "Yemi", "Tunde", "Zara", "Malik", "Sade", "Olu", "Bisi", "Femi", + "James", "Maria", "John", "Sarah", "Michael", "Emma", "David", "Olivia", + "Daniel", "Sophia", "Matthew", "Isabella", "Andrew", "Mia", "Joshua", + "Charlotte", "Ryan", "Amelia", "Nathan", "Harper", "Tyler", "Evelyn", + "Arjun", "Priya", "Rahul", "Ananya", "Vikram", "Deepa", "Rohan", "Kavya", + "Wei", "Mei", "Jun", "Ling", "Hao", "Xiu", "Feng", "Yan", + "Carlos", "Sofia", "Miguel", "Valentina", "Diego", "Camila", "Luis", "Ana", + "Yuki", "Hana", "Kenji", "Sakura", "Takeshi", "Aiko", "Hiroshi", "Yuna", +] + +LAST_NAMES = [ + "Okafor", "Mensah", "Diallo", "Nkosi", "Osei", "Adeyemi", "Kamara", + "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", + "Sharma", "Patel", "Singh", "Kumar", "Gupta", "Verma", + "Wang", "Li", "Zhang", "Liu", "Chen", "Yang", "Huang", + "Silva", "Santos", "Oliveira", "Souza", "Costa", "Ferreira", + "Tanaka", "Suzuki", "Watanabe", "Yamamoto", "Nakamura", "Kobayashi", + "Mueller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", +] + + +def age_group_from_age(age: int) -> str: + if age <= 12: + return "child" + elif age <= 19: + return "teenager" + elif age <= 59: + return "adult" + else: + return "senior" + + +def generate_row(index: int) -> dict: + # Use index + short uuid suffix to guarantee uniqueness across runs + first = random.choice(FIRST_NAMES) + last = random.choice(LAST_NAMES) + suffix = uuid.uuid4().hex[:6] + name = f"{first} {last} {suffix}" + + gender = random.choice(GENDERS) + age = random.randint(1, 85) + age_group = age_group_from_age(age) + country_id, country_name = random.choice(COUNTRIES) + gender_probability = round(random.uniform(0.55, 0.99), 4) + country_probability = round(random.uniform(0.10, 0.95), 4) + + return { + "name": name, + "gender": gender, + "age": age, + "age_group": age_group, + "country_id": country_id, + "country_name": country_name, + "gender_probability": gender_probability, + "country_probability": country_probability, + } + + +def main(): + parser = argparse.ArgumentParser(description="Generate test CSV for profile ingestion") + parser.add_argument("--rows", type=int, default=500_000, help="Number of rows to generate") + parser.add_argument("--output", type=str, default="test_profiles.csv", help="Output file path") + args = parser.parse_args() + + output_path = Path(args.output) + fieldnames = [ + "name", "gender", "age", "age_group", + "country_id", "country_name", + "gender_probability", "country_probability", + ] + + print(f"Generating {args.rows:,} rows → {output_path}") + + with open(output_path, "w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + + for i in range(args.rows): + writer.writerow(generate_row(i)) + + if (i + 1) % 50_000 == 0: + print(f" {i + 1:,} rows written...") + + print(f"Done. File written to {output_path}") + + +if __name__ == "__main__": + main() diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 05736f2..595bf71 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -6,6 +6,7 @@ import jwt from "jsonwebtoken"; import * as uuid from "uuid"; import { DatabaseClient } from "../db"; import { parseExpiryMs } from "../utils"; +import { redis } from "../lib/redis"; config(); @@ -39,10 +40,10 @@ const dbClient = new DatabaseClient(); // Temporary in-memory store for PKCE verifiers and state // key: state string, value: { codeVerifier, expiresAt } -const pkceStore = new Map< - string, - { codeVerifier: string; expiresAt: number } ->(); +// const pkceStore = new Map< +// string, +// { codeVerifier: string; expiresAt: number } +// >(); function generateCodeVerifier(): string { return crypto.randomBytes(32).toString("base64url"); @@ -58,10 +59,11 @@ export async function githubRedirect(req: Request, res: Response) { const state = crypto.randomBytes(16).toString("hex"); // Store verifier keyed by state — expires in 10 minutes - pkceStore.set(state, { - codeVerifier, - expiresAt: Date.now() + 10 * 60 * 1000, - }); + // pkceStore.set(state, { + // codeVerifier, + // expiresAt: Date.now() + 10 * 60 * 1000, + // }); + await redis.set(`pkce:${state}`, JSON.stringify({ codeVerifier }), "EX", 600); const params = new URLSearchParams({ client_id: githubClientId as string, @@ -100,20 +102,27 @@ export async function githubCallback(req: Request, res: Response) { .json({ status: "error", message: "Missing state parameter" }); } - const pkceEntry = pkceStore.get(state); - if (!pkceEntry) { - return res - .status(400) - .json({ status: "error", message: "Invalid or expired state" }); - } - if (Date.now() > pkceEntry.expiresAt) { - pkceStore.delete(state); - return res - .status(400) - .json({ status: "error", message: "State expired, please try again" }); + // const pkceEntry = pkceStore.get(state); + const raw = await redis.get(`pkce:${state}`); + if (!raw) { + return res.status(400).json({ + status: "error", + message: "Invalid/expired state", + }); } - - pkceStore.delete(state); // one-time use + // if (!pkceEntry) { + // return res + // .status(400) + // .json({ status: "error", message: "Invalid or expired state" }); + // } + // if (Date.now() > pkceEntry.expiresAt) { + // pkceStore.delete(state); + // return res + // .status(400) + // .json({ status: "error", message: "State expired, please try again" }); + // } + + // pkceStore.delete(state); // one-time use if (!qCode || typeof qCode !== "string") { return res @@ -122,7 +131,10 @@ export async function githubCallback(req: Request, res: Response) { } code = qCode; - codeVerifier = pkceEntry.codeVerifier; + // codeVerifier = pkceEntry.codeVerifier; + const { codeVerifier: verifier } = JSON.parse(raw); + codeVerifier = verifier; + await redis.del(`pkce:${state}`); // for one time use } try { @@ -410,4 +422,4 @@ export async function me(req: Request, res: Response) { } } -export { pkceStore }; +// export { pkceStore }; diff --git a/src/controllers/profiles.controller.ts b/src/controllers/profiles.controller.ts index 71915fd..6019c9e 100644 --- a/src/controllers/profiles.controller.ts +++ b/src/controllers/profiles.controller.ts @@ -15,7 +15,9 @@ import { isGender, isSortField, isSortOrder, + normalizeQueryOptions, } from "../utils"; +import { redis } from "../lib/redis"; const dbClient = new DatabaseClient(); @@ -38,11 +40,16 @@ export async function createProfile(req: Request, res: Response) { } try { - const genderizeRes = await fetch(`https://api.genderize.io?name=${name}`); - const nationalizeRes = await fetch( - `https://api.nationalize.io/?name=${name}`, - ); - const agifyRes = await fetch(`https://api.agify.io/?name=${name}`); + const [genderizeRes, nationalizeRes, agifyRes] = await Promise.all([ + fetch(`https://api.genderize.io?name=${name}`), + fetch(`https://api.nationalize.io/?name=${name}`), + fetch(`https://api.agify.io/?name=${name}`), + ]); + // const genderizeRes = await fetch(`https://api.genderize.io?name=${name}`); + // const nationalizeRes = await fetch( + // `https://api.nationalize.io/?name=${name}`, + // ); + // const agifyRes = await fetch(`https://api.agify.io/?name=${name}`); if (!genderizeRes.ok) { return res.status(502).json({ @@ -65,11 +72,22 @@ export async function createProfile(req: Request, res: Response) { }); } - const { - count: sample_size, - probability: gender_probability, - gender, - }: GenderizeAPIResponse = await genderizeRes.json(); + const [ + { count: sample_size, probability: gender_probability, gender }, + { country }, + { age }, + ]: [GenderizeAPIResponse, NationalizeAPIResponse, AgifyAPIResponse] = + await Promise.all([ + genderizeRes.json(), + nationalizeRes.json(), + agifyRes.json(), + ]); + + // const { + // count: sample_size, + // probability: gender_probability, + // gender, + // }: GenderizeAPIResponse = await genderizeRes.json(); if (gender === null || sample_size === 0) { return res.status(502).json({ @@ -78,7 +96,7 @@ export async function createProfile(req: Request, res: Response) { }); } - const { age }: AgifyAPIResponse = await agifyRes.json(); + // const { age }: AgifyAPIResponse = await agifyRes.json(); if (age === null || age < 0) { return res.status(502).json({ @@ -87,7 +105,7 @@ export async function createProfile(req: Request, res: Response) { }); } - const { country }: NationalizeAPIResponse = await nationalizeRes.json(); + // const { country }: NationalizeAPIResponse = await nationalizeRes.json(); const age_group: AgeGroup = age <= 12 @@ -322,10 +340,19 @@ export async function getAllProfiles(req: Request, res: Response) { } try { + const cacheKey = `profiles:${normalizeQueryOptions(options)}`; + const cached = await redis.get(cacheKey); + + if (cached) { + const parsed = JSON.parse(cached); + return res.status(200).json(parsed); + } + const { page, limit, total, records } = await dbClient.getAllRecords(options); const totalPages = Math.ceil(total / limit); - return res.status(200).json({ + + const responseBody = { status: "success", page, limit, @@ -338,7 +365,11 @@ export async function getAllProfiles(req: Request, res: Response) { page > 1 ? `${baseUrl}/${path}?page=${page}&limit=${limit}` : null, }, data: records, - }); + }; + + await redis.set(cacheKey, JSON.stringify(responseBody), "EX", 60); + + return res.status(200).json(responseBody); // return res.status(200).json({ // status: "success", // data: records, @@ -380,10 +411,19 @@ export async function searchForProfiles(req: Request, res: Response) { } try { + const cacheKey = `profiles:${normalizeQueryOptions(options)}`; + const cached = await redis.get(cacheKey); + + if (cached) { + const parsed = JSON.parse(cached); + return res.status(200).json(parsed); + } + const { page, limit, total, records } = await dbClient.getAllRecords(options); const totalPages = Math.ceil(total / limit); - return res.status(200).json({ + + const responseBody = { status: "success", page, limit, @@ -396,7 +436,11 @@ export async function searchForProfiles(req: Request, res: Response) { page > 1 ? `${baseUrl}/${path}?page=${page}&limit=${limit}` : null, }, data: records, - }); + }; + + await redis.set(cacheKey, JSON.stringify(responseBody), "EX", 60); + + return res.status(200).json(responseBody); // return res.status(200).json({ // status: "success", // data: records, diff --git a/src/controllers/upload.controller.ts b/src/controllers/upload.controller.ts new file mode 100644 index 0000000..b7038ec --- /dev/null +++ b/src/controllers/upload.controller.ts @@ -0,0 +1,202 @@ +import Busboy from "busboy"; +import { parse } from 'csv-parse' +import { Request, Response } from "express"; +import { AgeGroup, InsertRecord, RawCSVRow } from "../types"; +import { DatabaseClient } from "../db"; +import { v7 } from "uuid"; +import { countryMap, countryNameMap, isAgeGroup, isGender } from "../utils"; + +const dbClient = new DatabaseClient(); + +const parseCSVField = (row: RawCSVRow): { + kind: "error" | "success", + response: InsertRecord | string +} => { + if (!row.name || typeof row.name !== 'string' || row.name.length === 0) { + return { kind: "error", response: "Name not found" }; + } + const name = row.name + + if (!row.gender || typeof row.gender !== 'string' || !isGender(row.gender)) { + return { kind: "error", response: "Invalid Gender" }; + } + const gender = row.gender; + + if (!row.gender_probability || typeof row.gender_probability !== 'string' || isNaN(parseFloat(row.gender_probability))) { + return { kind: "error", response: "Invalid Gender probability" } + } + const gender_probability = parseFloat(row.gender_probability); + + if (!row.age || typeof row.age !== 'string' || isNaN(parseInt(row.age))) { + return { kind: "error", response: "Invalid age" } + } + const age = parseInt(row.age); + if (age < 0 ) return { kind: "error", response: "Invalid age" } + + let age_group: AgeGroup = "" as AgeGroup; + + if (!row.age_group || typeof row.age_group !== 'string' || !isAgeGroup(row.age_group)) { + age_group = age <= 12 + ? "child" + : age <= 19 + ? "teenager" + : age <= 59 + ? "adult" + : "senior"; + } else { + age_group = row.age_group; + } + + let country_id: string = "" + if (row.country_id !== undefined) { + if (row.country_id && typeof row.country_id === 'string' && countryMap.has(row.country_id.toUpperCase())) { + country_id = row.country_id.toUpperCase(); + } else if (row.country_name && typeof row.country_name === 'string') { + const found = countryNameMap.get(row.country_name.toLowerCase()); + if (found) { + country_id = found; + } else { + return { kind: "error", response: "Invalid country ID" }; + } + } else { + return { kind: "error", response: "Invalid country ID" }; + } + + }; + + if (country_id.length === 0) return { kind: "error", response: "Invalid country ID" } + + // let country_name: string = ""; + const country_name = countryMap.get(country_id); + if (!country_name) return { kind: "error", response: "Invalid country name" }; + + if (!row.country_probability || typeof row.country_probability !== 'string' || isNaN(parseFloat(row.country_probability))) { + return { kind: "error", response: "Invalid Gender probability" } + } + const country_probability = parseFloat(row.country_probability); + + return { + kind: "success", + response: { + id: v7(), + name, + gender, + gender_probability, + age, + age_group, + country_id, + country_name, + country_probability, + } + } +} + +export function handleCSVUpload(req: Request, res: Response) { + const busboy = Busboy({ headers: req.headers }); + + const stats = { + total_rows: 0, + inserted: 0, + skipped: 0, + reasons: { + duplicate_name: 0, + invalid_age: 0, + invalid_gender: 0, + invalid_country: 0, + missing_fields: 0 + } + }; + + const batch: { + id: string; + name: string; + gender: "male" | "female"; + gender_probability: number; + // sample_size: number; + age: number; + age_group: "adult" | "child" | "teenager" | "senior"; + country_id: string; + country_name: string; + country_probability: number; + }[] = []; + const BATCH_SIZE = 5000; + + let processStreamFunction: Promise; + + busboy.on('file', (_fieldname, file, _info) => { + const parser = parse({ columns: true }); + + const processStream = new Promise((resolve, reject) => { + parser.on('data', (row: RawCSVRow) => { + stats.total_rows++; + const recordOrError = parseCSVField(row); + + if (recordOrError.kind === "success") { + batch.push(recordOrError.response as InsertRecord) + } else { + stats.skipped = stats.skipped + 1; + if (recordOrError.response === "Invalid age") { + stats.reasons.invalid_age++; + } else if (recordOrError.response === "Invalid Gender") { + stats.reasons.invalid_gender++; + } else if (recordOrError.response === "Invalid country name") { + stats.reasons.invalid_country++; + } else { + stats.reasons.missing_fields++ + } + } + + + // sync validation and accumulation only + + }); + + parser.on('end', async () => { + // flush remaining batch here + const chunks = []; + const CHUNKS_SIZE = 15; // number of chunks to do at once. If you do all, pg fails + for (let i = 0; i < batch.length; i += BATCH_SIZE) { + chunks.push(batch.slice(i, i + BATCH_SIZE)); + } + // const results = await Promise.all(chunks.map(chunk => dbClient.batchInsertRecords(chunk))); + + for (let i = 0; i < chunks.length; i += CHUNKS_SIZE) { + const current_chunk_group = chunks.slice(i, i + CHUNKS_SIZE); + const results = await Promise.all(current_chunk_group.map(grp => dbClient.batchInsertRecords(grp))); + + for (const result of results) { + stats.inserted += result.inserted; + stats.reasons.duplicate_name += result.duplicates; + stats.skipped += result.duplicates + } + } + + // for (let i = 0; i < batch.length; i += BATCH_SIZE) { + // const chunk = batch.slice(i, i + BATCH_SIZE); + // const result = await dbClient.batchInsertRecords(chunk) + // stats.inserted += result.inserted; + // stats.reasons.duplicate_name += result.duplicates; + // stats.skipped += result.duplicates; + // } + batch.length = 0; + resolve(); + }) + + parser.on('error', reject); + }) + + file.pipe(parser) + + processStreamFunction = processStream; + }) + + busboy.on('finish', async () => { + await processStreamFunction; + return res.status(201).json({ + status: "success", + ...stats + }) + }) + + req.pipe(busboy); +} \ No newline at end of file diff --git a/src/db/index.ts b/src/db/index.ts index ec5ad1b..61a1876 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -15,21 +15,35 @@ config(); * PostgreSQL database client for interactions */ export class DatabaseClient { - private pool: Pool; + private primaryPool: Pool; + private replicaPool: Pool; constructor() { - const dbUrl = process.env.CLASSIFY_DB_URL; + const primaryDbUrl = process.env.CLASSIFY_DB_URL; + const replicaDbUrl = process.env.CLASSIFY_DB_REPLICA_URL ?? primaryDbUrl; - if (!dbUrl) { + if (!replicaDbUrl) { throw new Error("CLASSIFY_DB_URL environment variable not set"); } - this.pool = new Pool({ - connectionString: dbUrl, + const poolConfig = { + // connectionString: dbUrl, ssl: process.env.NODE_ENV === "production" ? { rejectUnauthorized: false } : false, + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, + }; + + this.primaryPool = new Pool({ + connectionString: primaryDbUrl, + ...poolConfig, + }); + this.replicaPool = new Pool({ + connectionString: replicaDbUrl, + ...poolConfig, }); } @@ -59,45 +73,31 @@ export class DatabaseClient { id, name, gender, gender_probability, age, age_group, country_id, country_name, country_probability ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9 - ) ON CONFLICT (name) DO NOTHING - RETURNING id, name, gender, gender_probability, age, age_group, country_id, country_name, country_probability, created_at + ) ON CONFLICT (name) DO UPDATE + SET id = classifications.id + RETURNING *, (xmax = 0) AS inserted `; - try { - const result: QueryResult = await this.pool.query(query, [ - record.id, - record.name, - record.gender, - record.gender_probability, - // record.sample_size, - record.age, - record.age_group, - record.country_id, - record.country_name, - record.country_probability, - ]); + const result = await this.primaryPool.query(query, [ + record.id, + record.name, + record.gender, + record.gender_probability, + // record.sample_size, + record.age, + record.age_group, + record.country_id, + record.country_name, + record.country_probability, + ]); + + const row = result.rows[0]; + const { inserted, ...classification } = row; - if (result.rowCount && result.rowCount > 0) { - return { - classification: result.rows[0], - duplicate: false, - }; - } - - const existing = await this.getRecordByName(record.name); - if (!existing) { - throw new Error("Unexpected state: name conflict but record not found"); - } - return { - classification: existing, - duplicate: true, - }; - } catch (error: any) { - if (error.code === "23505") { - throw new Error("Name already exists"); - } - throw error; - } + return { + classification: classification as Classification, + duplicate: !inserted, + }; } async getRecordByName(name: string): Promise { @@ -109,7 +109,9 @@ export class DatabaseClient { WHERE LOWER(name) = $1 `; - const result = await this.pool.query(query, [name.trim().toLowerCase()]); + const result = await this.replicaPool.query(query, [ + name.trim().toLowerCase(), + ]); if (result.rowCount && result.rowCount > 0) { return result.rows[0]; @@ -132,7 +134,7 @@ export class DatabaseClient { WHERE id = $1 `; - const result = await this.pool.query(query, [id]); + const result = await this.replicaPool.query(query, [id]); if (result.rowCount && result.rowCount > 0) { return result.rows[0]; @@ -142,7 +144,8 @@ export class DatabaseClient { } async close(): Promise { - this.pool.end(); + this.primaryPool.end(); + this.replicaPool.end(); } async getAllRecords(options: AllProfileQueryOptions): Promise<{ @@ -178,8 +181,8 @@ export class DatabaseClient { } if (options.country_id) { - conditions.push(`LOWER(country_id) = $${paramIndex}`); - values.push(options.country_id.toLowerCase()); + conditions.push(`country_id = $${paramIndex}`); + values.push(options.country_id.toUpperCase()); paramIndex++; } @@ -217,26 +220,31 @@ export class DatabaseClient { const sortClause = sortBy && sortOrder ? ` ORDER BY ${sortBy} ${sortOrder} ` : ``; + const countQuery = `SELECT COUNT(*) FROM classifications ${whereClause}`; + const query = ` SELECT id, name, gender, gender_probability, age, age_group, - country_id, country_name, country_probability, created_at, - COUNT(*) OVER() AS total_count + country_id, country_name, country_probability, created_at FROM classifications ${whereClause} ${sortClause} LIMIT $${paramIndex} OFFSET $${paramIndex + 1} `; - // const total = parseInt(totalResult.rows[0].count); + // filterValues: only WHERE clause params — passed to both queries + // dataValues: filter params + limit + offset — passed only to the data query + const filterValues = [...values]; + const dataValues = [...values, limit, offset]; - values.push(limit, offset); + const [result, countResult] = await Promise.all([ + this.replicaPool.query(query, dataValues), + this.replicaPool.query(countQuery, filterValues), + ]); - const result = await this.pool.query(query, values); - const total = - result.rows.length > 0 ? parseInt(result.rows[0].total_count) : 0; + const total = parseInt(countResult.rows[0].count); - const data = result.rows.map(({ total_count, ...row }) => row); + const data = result.rows.map(({ ...row }) => row); return { records: data, // count: result.rowCount ?? 0, @@ -253,7 +261,7 @@ export class DatabaseClient { `; try { - const result = await this.pool.query(query, [id]); + const result = await this.primaryPool.query(query, [id]); if (result.rowCount === 0) { throw new Error(`Record with id "${id}" not found`); @@ -274,7 +282,7 @@ export class DatabaseClient { avatar_url: string | null; last_login_at: Date; }): Promise { - const result = await this.pool.query( + const result = await this.primaryPool.query( `INSERT INTO users (id, github_id, username, email, avatar_url, last_login_at) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (github_id) DO UPDATE @@ -301,7 +309,7 @@ export class DatabaseClient { token_hash: string; expires_at: Date; }): Promise { - const result = await this.pool.query( + const result = await this.primaryPool.query( `INSERT INTO sessions (id, user_id, token_hash, expires_at) VALUES ($1, $2, $3, $4) RETURNING id, user_id, token_hash, expires_at, revoked, created_at`, @@ -311,7 +319,7 @@ export class DatabaseClient { } async getSessionByTokenHash(tokenHash: string): Promise { - const result = await this.pool.query( + const result = await this.primaryPool.query( `SELECT id, user_id, token_hash, expires_at, revoked, created_at FROM sessions WHERE token_hash = $1`, @@ -321,18 +329,68 @@ export class DatabaseClient { } async revokeSession(tokenHash: string): Promise { - await this.pool.query( + await this.primaryPool.query( `UPDATE sessions SET revoked = TRUE WHERE token_hash = $1`, [tokenHash], ); } async getUserById(id: string): Promise { - const result = await this.pool.query( + const result = await this.primaryPool.query( `SELECT id, github_id, username, email, avatar_url, role, is_active, last_login_at, created_at FROM users WHERE id = $1`, [id], ); return result.rowCount && result.rowCount > 0 ? result.rows[0] : null; } + + async batchInsertRecords( + records: { + id: string; + name: string; + gender: "male" | "female"; + gender_probability: number; + // sample_size: number; + age: number; + age_group: "adult" | "child" | "teenager" | "senior"; + country_id: string; + country_name: string; + country_probability: number; + }[], + ): Promise<{ + inserted: number; + duplicates: number; + }> { + if (records.length === 0) return { inserted: 0, duplicates: 0 }; + + const values: any[] = []; + const placeholders = records.map((record, i) => { + const base = i * 9; + values.push( + record.id, + record.name, + record.gender, + record.gender_probability, + record.age, + record.age_group, + record.country_id, + record.country_name, + record.country_probability, + ); + return `($${base+1}, $${base+2}, $${base+3}, $${base+4}, $${base+5},$${base+6},$${base+7},$${base+8},$${base+9})` + }); + + const query = ` + INSERT INTO classifications + (id, name, gender, gender_probability, age, age_group, country_id, country_name, country_probability) + VALUES ${placeholders.join(",")} + ON CONFLICT (name) DO NOTHING + `; + + const result = await this.primaryPool.query(query, values); + const inserted = result.rowCount ?? 0; + const duplicates = records.length - inserted; + + return { inserted, duplicates } + } } diff --git a/src/lib/redis.ts b/src/lib/redis.ts new file mode 100644 index 0000000..90c3656 --- /dev/null +++ b/src/lib/redis.ts @@ -0,0 +1,12 @@ +import Redis from "ioredis"; +import { config } from "dotenv"; + +config(); + +const redisUrl = process.env.REDIS_URL; + +if (!redisUrl) { + throw new Error("REDIS_URL envrionment variable not set"); +} + +export const redis = new Redis(redisUrl); diff --git a/src/middleware/authenticate.ts b/src/middleware/authenticate.ts index 2f373d6..51ffc44 100644 --- a/src/middleware/authenticate.ts +++ b/src/middleware/authenticate.ts @@ -79,10 +79,14 @@ export async function checkActive( } const user = await getDbClient().getUserById(req.user.userId); if (!user) { - return res.status(401).json({ status: "error", message: "Invalid access token" }); + return res + .status(401) + .json({ status: "error", message: "Invalid access token" }); } if (!user.is_active) { - return res.status(403).json({ status: "error", message: "Deactivated User" }); + return res + .status(403) + .json({ status: "error", message: "Deactivated User" }); } next(); } diff --git a/src/routes/v1/profiles.route.ts b/src/routes/v1/profiles.route.ts index 0bcae30..f00ef21 100644 --- a/src/routes/v1/profiles.route.ts +++ b/src/routes/v1/profiles.route.ts @@ -7,8 +7,8 @@ import { getProfile, searchForProfiles, } from "../../controllers/profiles.controller"; -import { authenticate } from "../../middleware/authenticate"; import { authorize } from "../../middleware/authorize"; +import { handleCSVUpload } from "../../controllers/upload.controller"; const router = Router(); @@ -16,6 +16,8 @@ router.post("/", authorize("admin"), createProfile); router.get("/", authorize("analyst"), getAllProfiles); +router.post("/upload", authorize("admin"), handleCSVUpload); + router.get("/search", authorize("analyst"), searchForProfiles); router.get("/export", authorize("analyst"), exportCSV); diff --git a/src/types.ts b/src/types.ts index 125b65b..d0fce0b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -88,3 +88,27 @@ export type Session = { revoked: boolean; created_at: Date; }; + +export type RawCSVRow = { + name?: string; + gender?: string; + age?: string; + age_group?: string; + country_id?: string; + country_name?: string; + gender_probability?: string; + country_probability?: string; +}; + +export type InsertRecord = { + id: string; + name: string; + gender: Gender; + gender_probability: number; + // sample_size: number; + age: number; + age_group: AgeGroup; + country_id: string; + country_name: string; + country_probability: number; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 9add25d..88a2146 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -239,3 +239,29 @@ export function parseExpiryMs(expiry: string): number { throw new Error(`Unknown expiry unit: ${unit}`); } } + +export function normalizeQueryOptions(options: AllProfileQueryOptions): string { + const normalized: Record = {}; + + const keyOrder: (keyof AllProfileQueryOptions)[] = [ + "gender", + "age_group", + "country_id", + "min_age", + "max_age", + "min_gender_probability", + "min_country_probability", + "sort_by", + "sort_order", + "page", + "limit", + ]; + + for (const key of keyOrder) { + if (options[key] !== undefined) { + normalized[key] = options[key]; + } + } + + return JSON.stringify(normalized); +} diff --git a/tests/db/auth.test.ts b/tests/db/auth.test.ts index d44d92c..71b86ea 100644 --- a/tests/db/auth.test.ts +++ b/tests/db/auth.test.ts @@ -20,9 +20,10 @@ beforeAll(() => { afterAll(async () => { // Clean up test user (cascades to sessions) - await (db as any).pool.query(`DELETE FROM users WHERE github_id = $1`, [ - testGithubId, - ]); + await (db as any).primaryPool.query( + `DELETE FROM users WHERE github_id = $1`, + [testGithubId], + ); }); // ─── upsertUser ──────────────────────────────────────────────────────────── @@ -70,14 +71,15 @@ describe("upsertUser", () => { expect(user.email).toBeNull(); // cleanup - await (db as any).pool.query(`DELETE FROM users WHERE github_id = $1`, [ - githubId, - ]); + await (db as any).primaryPool.query( + `DELETE FROM users WHERE github_id = $1`, + [githubId], + ); }); it("preserves the role field (does not reset to default on update)", async () => { // Manually set role to admin - await (db as any).pool.query( + await (db as any).primaryPool.query( `UPDATE users SET role = 'admin' WHERE github_id = $1`, [testGithubId], ); @@ -92,7 +94,7 @@ describe("upsertUser", () => { expect(updated.role).toBe("admin"); // role not overwritten by upsert // reset back to analyst - await (db as any).pool.query( + await (db as any).primaryPool.query( `UPDATE users SET role = 'analyst' WHERE github_id = $1`, [testGithubId], ); diff --git a/tests/middleware/authenticate.test.ts b/tests/middleware/authenticate.test.ts index 9977209..f2d2485 100644 --- a/tests/middleware/authenticate.test.ts +++ b/tests/middleware/authenticate.test.ts @@ -23,7 +23,7 @@ vi.mock("../../src/db", () => { function makeReq(authHeader?: string): Partial { return { headers: authHeader - ? { authorization: authHeader, 'x-client-type': 'cli' } + ? { authorization: authHeader, "x-client-type": "cli" } : {}, cookies: {}, } as Partial; diff --git a/tests/utils/normalizeQueryOptions.test.ts b/tests/utils/normalizeQueryOptions.test.ts new file mode 100644 index 0000000..b5e3bf1 --- /dev/null +++ b/tests/utils/normalizeQueryOptions.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect } from "vitest"; +import { normalizeQueryOptions } from "../../src/utils"; +import { AllProfileQueryOptions } from "../../src/types"; + +describe("normalizeQueryOptions", () => { + it("produces identical output for the same filters regardless of insertion order", () => { + const a: AllProfileQueryOptions = { + country_id: "NG", + gender: "female", + min_age: 20, + }; + const b: AllProfileQueryOptions = { + min_age: 20, + gender: "female", + country_id: "NG", + }; + expect(normalizeQueryOptions(a)).toBe(normalizeQueryOptions(b)); + }); + + it("excludes undefined fields from the output", () => { + const options: AllProfileQueryOptions = { gender: "male" }; + const result = normalizeQueryOptions(options); + const parsed = JSON.parse(result); + expect(Object.keys(parsed)).toEqual(["gender"]); + }); + + it("produces different output for different filters", () => { + const a = normalizeQueryOptions({ gender: "male" }); + const b = normalizeQueryOptions({ gender: "female" }); + expect(a).not.toBe(b); + }); + + it("includes all defined fields in canonical key order", () => { + const options: AllProfileQueryOptions = { + limit: 10, + page: 1, + gender: "male", + country_id: "KE", + min_age: 18, + max_age: 35, + age_group: "adult", + sort_by: "age", + sort_order: "asc", + min_gender_probability: 0.8, + min_country_probability: 0.7, + }; + const result = JSON.parse(normalizeQueryOptions(options)); + const keys = Object.keys(result); + expect(keys).toEqual([ + "gender", + "age_group", + "country_id", + "min_age", + "max_age", + "min_gender_probability", + "min_country_probability", + "sort_by", + "sort_order", + "page", + "limit", + ]); + }); + + it("empty options produces empty JSON object", () => { + expect(normalizeQueryOptions({})).toBe("{}"); + }); + + it("two semantically equivalent NLQ-parsed results produce the same key", () => { + // "Nigerian females between 20 and 45" and "women aged 20-45 from Nigeria" + // both parse to the same filter set + const a: AllProfileQueryOptions = { + gender: "female", + country_id: "NG", + min_age: 20, + max_age: 45, + }; + const b: AllProfileQueryOptions = { + min_age: 20, + country_id: "NG", + max_age: 45, + gender: "female", + }; + expect(normalizeQueryOptions(a)).toBe(normalizeQueryOptions(b)); + }); +}); diff --git a/tests/v1/auth.test.ts b/tests/v1/auth.test.ts index f3f8807..282aa4c 100644 --- a/tests/v1/auth.test.ts +++ b/tests/v1/auth.test.ts @@ -15,10 +15,10 @@ import { logout, me, githubRedirect, - pkceStore, } from "../../src/controllers/auth.controller"; import { authenticate, checkActive } from "../../src/middleware/authenticate"; import { DatabaseClient } from "../../src/db"; +import { redis } from "../../src/lib/redis"; const JWT_SECRET = process.env.JWT_SECRET!; @@ -52,7 +52,7 @@ async function createTestUser(role: "analyst" | "admin" = "analyst") { last_login_at: new Date(), }); if (role === "admin") { - await (db as any).pool.query( + await (db as any).primaryPool.query( `UPDATE users SET role = 'admin' WHERE id = $1`, [user.id], ); @@ -78,7 +78,9 @@ const createdUserIds: string[] = []; afterAll(async () => { for (const id of createdUserIds) { - await (db as any).pool.query(`DELETE FROM users WHERE id = $1`, [id]); + await (db as any).primaryPool.query(`DELETE FROM users WHERE id = $1`, [ + id, + ]); } }); @@ -100,10 +102,17 @@ describe("GET /api/v1/auth/github", () => { expect(location).toContain("code_challenge_method=S256"); }); - it("stores state in pkceStore", async () => { - const sizeBefore = pkceStore.size; - await request(app).get("/api/v1/auth/github"); - expect(pkceStore.size).toBe(sizeBefore + 1); + it("stores state in Redis with pkce: prefix", async () => { + const res = await request(app).get("/api/v1/auth/github"); + const location = res.headers.location as string; + const url = new URL(location); + const state = url.searchParams.get("state")!; + const stored = await redis.get(`pkce:${state}`); + expect(stored).not.toBeNull(); + const parsed = JSON.parse(stored!); + expect(parsed.codeVerifier).toBeDefined(); + // cleanup + await redis.del(`pkce:${state}`); }); }); @@ -118,54 +127,47 @@ describe("GET /api/v1/auth/github/callback", () => { expect(res.body.message).toContain("state"); }); - it("returns 400 when state is invalid (not in pkceStore)", async () => { + it("returns 400 when state is invalid (not in Redis)", async () => { const res = await request(app) .get("/api/v1/auth/github/callback") .query({ code: "somecode", state: "invalidstate" }); expect(res.status).toBe(400); - expect(res.body.message).toContain("Invalid or expired state"); - }); - - it("returns 400 when state is expired", async () => { - const expiredState = "expiredstate123"; - pkceStore.set(expiredState, { - codeVerifier: "verifier", - expiresAt: Date.now() - 1000, - }); - - const res = await request(app) - .get("/api/v1/auth/github/callback") - .query({ code: "somecode", state: expiredState }); - expect(res.status).toBe(400); - expect(res.body.message).toContain("expired"); + expect(res.body.message).toContain("Invalid"); }); it("returns 400 when code is missing but state is valid", async () => { const state = "validstate123"; - pkceStore.set(state, { - codeVerifier: "verifier", - expiresAt: Date.now() + 60_000, - }); + await redis.set( + `pkce:${state}`, + JSON.stringify({ codeVerifier: "verifier" }), + "EX", + 600, + ); const res = await request(app) .get("/api/v1/auth/github/callback") .query({ state }); expect(res.status).toBe(400); expect(res.body.message).toContain("authorization code"); + + await redis.del(`pkce:${state}`); }); - it("consumes state from pkceStore (one-time use)", async () => { + it("consumes state from Redis (one-time use)", async () => { const state = "onetimestate"; - pkceStore.set(state, { - codeVerifier: "verifier", - expiresAt: Date.now() + 60_000, - }); + await redis.set( + `pkce:${state}`, + JSON.stringify({ codeVerifier: "verifier" }), + "EX", + 600, + ); await request(app) .get("/api/v1/auth/github/callback") .query({ code: "fakecode", state }); - expect(pkceStore.has(state)).toBe(false); + const remaining = await redis.get(`pkce:${state}`); + expect(remaining).toBeNull(); }); }); @@ -406,10 +408,12 @@ describe("GET /api/v1/auth/me", () => { describe("GET /api/v1/auth/github/callback — success path", () => { it("CLI path: returns access_token and refresh_token as JSON", async () => { const state = `success_state_${Date.now()}`; - pkceStore.set(state, { - codeVerifier: "test-verifier", - expiresAt: Date.now() + 60_000, - }); + await redis.set( + `pkce:${state}`, + JSON.stringify({ codeVerifier: "test-verifier" }), + "EX", + 600, + ); const githubUserId = Math.floor(Math.random() * 1_000_000); @@ -442,17 +446,20 @@ describe("GET /api/v1/auth/github/callback — success path", () => { expect(res.body.expires_in).toBe(180); // JWT_EXPIRY=3m = 180s const db2 = new DatabaseClient(); - await (db2 as any).pool.query(`DELETE FROM users WHERE github_id = $1`, [ - String(githubUserId), - ]); + await (db2 as any).primaryPool.query( + `DELETE FROM users WHERE github_id = $1`, + [String(githubUserId)], + ); }); it("CLI path: access token contains correct userId and role", async () => { const state = `success_state_role_${Date.now()}`; - pkceStore.set(state, { - codeVerifier: "test-verifier", - expiresAt: Date.now() + 60_000, - }); + await redis.set( + `pkce:${state}`, + JSON.stringify({ codeVerifier: "test-verifier" }), + "EX", + 600, + ); const githubUserId = Math.floor(Math.random() * 1_000_000) + 2_000_000; @@ -482,17 +489,20 @@ describe("GET /api/v1/auth/github/callback — success path", () => { expect(decoded.role).toBe("analyst"); const db2 = new DatabaseClient(); - await (db2 as any).pool.query(`DELETE FROM users WHERE github_id = $1`, [ - String(githubUserId), - ]); + await (db2 as any).primaryPool.query( + `DELETE FROM users WHERE github_id = $1`, + [String(githubUserId)], + ); }); it("returns 502 when GitHub token exchange fails", async () => { const state = `fail_state_${Date.now()}`; - pkceStore.set(state, { - codeVerifier: "test-verifier", - expiresAt: Date.now() + 60_000, - }); + await redis.set( + `pkce:${state}`, + JSON.stringify({ codeVerifier: "test-verifier" }), + "EX", + 600, + ); const originalFetch = global.fetch; global.fetch = vi.fn().mockResolvedValueOnce({ @@ -511,10 +521,12 @@ describe("GET /api/v1/auth/github/callback — success path", () => { it("returns 502 when GitHub user fetch fails (no id)", async () => { const state = `fail_user_state_${Date.now()}`; - pkceStore.set(state, { - codeVerifier: "test-verifier", - expiresAt: Date.now() + 60_000, - }); + await redis.set( + `pkce:${state}`, + JSON.stringify({ codeVerifier: "test-verifier" }), + "EX", + 600, + ); const originalFetch = global.fetch; global.fetch = vi @@ -538,10 +550,12 @@ describe("GET /api/v1/auth/github/callback — success path", () => { it("browser path: redirects to portal callback with tokens in query params", async () => { const state = `browser_state_${Date.now()}`; - pkceStore.set(state, { - codeVerifier: "test-verifier", - expiresAt: Date.now() + 60_000, - }); + await redis.set( + `pkce:${state}`, + JSON.stringify({ codeVerifier: "test-verifier" }), + "EX", + 600, + ); const githubUserId = Math.floor(Math.random() * 1_000_000) + 4_000_000; @@ -565,7 +579,6 @@ describe("GET /api/v1/auth/github/callback — success path", () => { global.fetch = originalFetch; - // Browser path redirects to portal callback with tokens as query params expect(res.status).toBe(302); expect(res.headers.location).toContain("/api/auth/callback"); expect(res.headers.location).toContain("access_token="); @@ -573,9 +586,10 @@ describe("GET /api/v1/auth/github/callback — success path", () => { expect(res.headers.location).toContain("csrf_token="); const db2 = new DatabaseClient(); - await (db2 as any).pool.query(`DELETE FROM users WHERE github_id = $1`, [ - String(githubUserId), - ]); + await (db2 as any).primaryPool.query( + `DELETE FROM users WHERE github_id = $1`, + [String(githubUserId)], + ); }); }); @@ -619,7 +633,9 @@ describe("POST /api/v1/auth/refresh — user deleted mid-session", () => { }); const rawToken2 = await createTestSession(user.id); - await (db as any).pool.query(`DELETE FROM users WHERE id = $1`, [user.id]); + await (db as any).primaryPool.query(`DELETE FROM users WHERE id = $1`, [ + user.id, + ]); const res = await request(app) .post("/api/v1/auth/refresh") diff --git a/tests/v1/cache.test.ts b/tests/v1/cache.test.ts new file mode 100644 index 0000000..2290fa8 --- /dev/null +++ b/tests/v1/cache.test.ts @@ -0,0 +1,168 @@ +import { describe, it, expect, afterEach } from "vitest"; +import request from "supertest"; +import express from "express"; +import jwt from "jsonwebtoken"; +import * as uuid from "uuid"; +import { config } from "dotenv"; + +config(); + +import { Router } from "express"; +import { + getAllProfiles, + searchForProfiles, +} from "../../src/controllers/profiles.controller"; +import { authenticate } from "../../src/middleware/authenticate"; +import { authorize } from "../../src/middleware/authorize"; +import { redis } from "../../src/lib/redis"; +import { normalizeQueryOptions } from "../../src/utils"; + +const JWT_SECRET = process.env.JWT_SECRET!; + +const profilesRouter = Router(); +profilesRouter.get("/", authenticate, authorize("analyst"), getAllProfiles); +profilesRouter.get( + "/search", + authenticate, + authorize("analyst"), + searchForProfiles, +); + +const app = express(); +app.use(express.json()); +app.use("/api/v1/profiles", profilesRouter); + +function analystToken(userId = uuid.v7()) { + return jwt.sign({ userId, role: "analyst" }, JWT_SECRET, { + expiresIn: "15m", + }); +} + +// Clean up any cache keys written during tests +afterEach(async () => { + const keys = await redis.keys("profiles:*"); + if (keys.length > 0) await redis.del(...keys); +}); + +// ─── Query result caching ────────────────────────────────────────────────── + +describe("GET /api/v1/profiles — caching", () => { + it("caches the response in Redis after first request", async () => { + const res = await request(app) + .get("/api/v1/profiles") + .query({ gender: "male", limit: 5 }) + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${analystToken()}`); + + expect(res.status).toBe(200); + + const cacheKey = `profiles:${normalizeQueryOptions({ gender: "male", limit: 5 })}`; + const cached = await redis.get(cacheKey); + expect(cached).not.toBeNull(); + + const parsed = JSON.parse(cached!); + expect(parsed.status).toBe("success"); + expect(parsed.data).toBeInstanceOf(Array); + }); + + it("returns cached response on second request (same result)", async () => { + const token = analystToken(); + + const first = await request(app) + .get("/api/v1/profiles") + .query({ gender: "female", limit: 5 }) + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${token}`); + + const second = await request(app) + .get("/api/v1/profiles") + .query({ gender: "female", limit: 5 }) + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${token}`); + + expect(second.status).toBe(200); + expect(second.body.data).toEqual(first.body.data); + expect(second.body.total).toBe(first.body.total); + }); + + it("different filter combinations produce different cache keys", async () => { + const keyA = `profiles:${normalizeQueryOptions({ gender: "male" })}`; + const keyB = `profiles:${normalizeQueryOptions({ gender: "female" })}`; + expect(keyA).not.toBe(keyB); + }); + + it("cache key is stable regardless of options object property order", () => { + const key1 = `profiles:${normalizeQueryOptions({ gender: "male", country_id: "NG", min_age: 20 })}`; + const key2 = `profiles:${normalizeQueryOptions({ min_age: 20, gender: "male", country_id: "NG" })}`; + expect(key1).toBe(key2); + }); +}); + +// ─── Search caching ──────────────────────────────────────────────────────── + +describe("GET /api/v1/profiles/search — caching", () => { + it("caches search results in Redis after first request", async () => { + const res = await request(app) + .get("/api/v1/profiles/search") + .query({ q: "young males" }) + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${analystToken()}`); + + expect(res.status).toBe(200); + + // "young males" parses to { gender: "male", min_age: 16, max_age: 24 } + const cacheKey = `profiles:${normalizeQueryOptions({ gender: "male", min_age: 16, max_age: 24 })}`; + const cached = await redis.get(cacheKey); + expect(cached).not.toBeNull(); + }); + + it("returns cached response on second search request", async () => { + const token = analystToken(); + + const first = await request(app) + .get("/api/v1/profiles/search") + .query({ q: "females above 30" }) + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${token}`); + + const second = await request(app) + .get("/api/v1/profiles/search") + .query({ q: "females above 30" }) + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${token}`); + + expect(second.status).toBe(200); + expect(second.body.total).toBe(first.body.total); + }); +}); + +// ─── PKCE Redis storage ──────────────────────────────────────────────────── + +describe("PKCE state — Redis storage", () => { + it("GET /auth/github stores state in Redis with pkce: prefix and TTL", async () => { + // Import the auth app inline to avoid circular issues + const { githubRedirect } = + await import("../../src/controllers/auth.controller"); + const authApp = express(); + const r = Router(); + r.get("/github", githubRedirect); + authApp.use("/auth", r); + + const res = await request(authApp).get("/auth/github"); + expect(res.status).toBe(302); + + const location = res.headers.location as string; + const url = new URL(location); + const state = url.searchParams.get("state")!; + + const stored = await redis.get(`pkce:${state}`); + expect(stored).not.toBeNull(); + + const ttl = await redis.ttl(`pkce:${state}`); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(600); + + // cleanup + await redis.del(`pkce:${state}`); + }); +}); diff --git a/tests/v1/profiles.test.ts b/tests/v1/profiles.test.ts index 47e929d..5cb39d9 100644 --- a/tests/v1/profiles.test.ts +++ b/tests/v1/profiles.test.ts @@ -442,7 +442,7 @@ describe("DELETE /api/v1/profiles/:id — success", () => { it("admin can delete an existing profile and gets 204", async () => { const db2 = new DatabaseClient(); const testId = uuid.v7(); - await (db2 as any).pool.query( + await (db2 as any).primaryPool.query( `INSERT INTO classifications (id, name, gender, gender_probability, age, age_group, country_id, country_name, country_probability) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [ diff --git a/tests/v1/upload.test.ts b/tests/v1/upload.test.ts new file mode 100644 index 0000000..54b3766 --- /dev/null +++ b/tests/v1/upload.test.ts @@ -0,0 +1,309 @@ +import { describe, it, expect, afterEach } from "vitest"; +import request from "supertest"; +import express from "express"; +import jwt from "jsonwebtoken"; +import * as uuid from "uuid"; +import { config } from "dotenv"; + +config(); + +import { Router } from "express"; +import { handleCSVUpload } from "../../src/controllers/upload.controller"; +import { authenticate } from "../../src/middleware/authenticate"; +import { authorize } from "../../src/middleware/authorize"; +import { DatabaseClient } from "../../src/db"; +import { buildCountryMap, countryMap } from "../../src/utils"; + +buildCountryMap(countryMap); + +const JWT_SECRET = process.env.JWT_SECRET!; +const db = new DatabaseClient(); + +const uploadRouter = Router(); +uploadRouter.post( + "/upload", + authenticate, + authorize("admin"), + handleCSVUpload, +); + +const app = express(); +app.use(express.json()); +app.use("/api/v1/profiles", uploadRouter); + +function adminToken(userId = uuid.v7()) { + return jwt.sign({ userId, role: "admin" }, JWT_SECRET, { expiresIn: "15m" }); +} + +function analystToken(userId = uuid.v7()) { + return jwt.sign({ userId, role: "analyst" }, JWT_SECRET, { + expiresIn: "15m", + }); +} + +// Helper to build a valid CSV buffer +function buildCSV(rows: Record[]): Buffer { + const header = + "name,gender,age,age_group,country_id,country_name,gender_probability,country_probability"; + const lines = rows.map((r) => + [ + r.name ?? "", + r.gender ?? "", + r.age ?? "", + r.age_group ?? "", + r.country_id ?? "", + r.country_name ?? "", + r.gender_probability ?? "", + r.country_probability ?? "", + ].join(","), + ); + return Buffer.from([header, ...lines].join("\n")); +} + +function validRow(overrides: Record = {}): Record { + return { + name: `TestPerson_${uuid.v7()}`, + gender: "female", + age: "28", + age_group: "adult", + country_id: "NG", + country_name: "Nigeria", + gender_probability: "0.95", + country_probability: "0.82", + ...overrides, + }; +} + +// Clean up inserted test records after each test +const insertedNames: string[] = []; +afterEach(async () => { + if (insertedNames.length > 0) { + const placeholders = insertedNames.map((_, i) => `$${i + 1}`).join(","); + await (db as any).primaryPool.query( + `DELETE FROM classifications WHERE name IN (${placeholders})`, + insertedNames, + ); + insertedNames.length = 0; + } +}); + +// ─── Auth & role enforcement ─────────────────────────────────────────────── + +describe("POST /api/v1/profiles/upload — auth & role", () => { + it("returns 401 with no token", async () => { + const csv = buildCSV([validRow()]); + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .attach("file", csv, "profiles.csv"); + expect(res.status).toBe(401); + }); + + it("returns 403 when analyst tries to upload", async () => { + const csv = buildCSV([validRow()]); + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${analystToken()}`) + .attach("file", csv, "profiles.csv"); + expect(res.status).toBe(403); + }); +}); + +// ─── Successful upload ───────────────────────────────────────────────────── + +describe("POST /api/v1/profiles/upload — success", () => { + it("returns 201 with correct summary shape", async () => { + const row = validRow(); + insertedNames.push(row.name); + const csv = buildCSV([row]); + + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + expect(res.status).toBe(201); + expect(res.body.status).toBe("success"); + expect(res.body.total_rows).toBeDefined(); + expect(res.body.inserted).toBeDefined(); + expect(res.body.skipped).toBeDefined(); + expect(res.body.reasons).toBeDefined(); + }); + + it("inserts valid rows and reports correct counts", async () => { + const rows = [validRow(), validRow(), validRow()]; + rows.forEach((r) => insertedNames.push(r.name)); + const csv = buildCSV(rows); + + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + expect(res.status).toBe(201); + expect(res.body.total_rows).toBe(3); + expect(res.body.inserted).toBe(3); + expect(res.body.skipped).toBe(0); + }); + + it("counts total_rows including skipped rows", async () => { + const goodRow = validRow(); + insertedNames.push(goodRow.name); + const badRow = validRow({ gender: "unknown" }); + const csv = buildCSV([goodRow, badRow]); + + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + expect(res.status).toBe(201); + expect(res.body.total_rows).toBe(2); + expect(res.body.inserted).toBe(1); + expect(res.body.skipped).toBe(1); + }); +}); + +// ─── Row validation ──────────────────────────────────────────────────────── + +describe("POST /api/v1/profiles/upload — row validation", () => { + it("skips rows with missing name", async () => { + const csv = buildCSV([validRow({ name: "" })]); + + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + expect(res.body.skipped).toBe(1); + expect(res.body.inserted).toBe(0); + expect(res.body.reasons.missing_fields).toBeGreaterThan(0); + }); + + it("skips rows with invalid gender", async () => { + const csv = buildCSV([validRow({ gender: "unknown" })]); + + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + expect(res.body.skipped).toBe(1); + expect(res.body.reasons.invalid_gender).toBe(1); + }); + + it("skips rows with negative age", async () => { + const csv = buildCSV([validRow({ age: "-5" })]); + + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + expect(res.body.skipped).toBe(1); + expect(res.body.reasons.invalid_age).toBe(1); + }); + + it("skips rows with invalid country", async () => { + const csv = buildCSV([ + validRow({ country_id: "XX", country_name: "Fakeland" }), + ]); + + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + expect(res.body.skipped).toBe(1); + }); + + it("skips duplicate names and reports them", async () => { + const row = validRow(); + insertedNames.push(row.name); + const csv = buildCSV([row]); + + // First upload — inserts + await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + // Second upload — duplicate + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + expect(res.body.reasons.duplicate_name).toBe(1); + expect(res.body.skipped).toBe(1); + expect(res.body.inserted).toBe(0); + }); + + it("a single bad row does not fail the entire upload", async () => { + const goodRow1 = validRow(); + const goodRow2 = validRow(); + insertedNames.push(goodRow1.name, goodRow2.name); + const csv = buildCSV([ + goodRow1, + validRow({ gender: "invalid" }), // bad row in the middle + goodRow2, + ]); + + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + expect(res.status).toBe(201); + expect(res.body.inserted).toBe(2); + expect(res.body.skipped).toBe(1); + expect(res.body.total_rows).toBe(3); + }); + + it("infers age_group from age when age_group column is missing or invalid", async () => { + const row = validRow({ age: "8", age_group: "" }); + insertedNames.push(row.name); + const csv = buildCSV([row]); + + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + expect(res.body.inserted).toBe(1); + expect(res.body.skipped).toBe(0); + }); +}); + +// ─── Empty file ──────────────────────────────────────────────────────────── + +describe("POST /api/v1/profiles/upload — edge cases", () => { + it("handles an empty CSV (header only) gracefully", async () => { + const csv = Buffer.from( + "name,gender,age,age_group,country_id,country_name,gender_probability,country_probability\n", + ); + + const res = await request(app) + .post("/api/v1/profiles/upload") + .set("x-client-type", "cli") + .set("Authorization", `Bearer ${adminToken()}`) + .attach("file", csv, "profiles.csv"); + + expect(res.status).toBe(201); + expect(res.body.total_rows).toBe(0); + expect(res.body.inserted).toBe(0); + expect(res.body.skipped).toBe(0); + }); +});