From 64082a4acb89ec34cdf9b81dde0a9c62b99ccdc7 Mon Sep 17 00:00:00 2001 From: david87131 Date: Wed, 29 Jul 2026 17:48:42 +0100 Subject: [PATCH] feat: add health check breakdown, per-user SSE cap, claimable cache invalidation test, api-types sync doc - Health endpoint now returns a `checks` object breaking down database, indexer lag, redis, and soroban RPC status individually, while keeping the top-level `status` field for simple uptime monitors. Updated the Postman collection's example response to match. - SSE subscriptions now enforce a per-authenticated-user concurrent connection cap (independent of the existing per-IP cap), returning 429 when exceeded. Added tests covering the cap, its independence from IP, and release on disconnect. - Added a claimable.service test that primes the cache, simulates an indexed TokensWithdrawn event, and asserts the next read reflects the new claimable amount immediately rather than waiting for the TTL. - Added an opt-in `codegen:api-types` dev script (openapi-typescript against the backend's /api-docs.json) and a doc comment on api-types.ts pointing at backend/src/config/swagger.ts as the source of truth. Not wired into CI and does not replace existing hand-written types/call sites. Fixes #1117 Fixes #1116 Fixes #1115 Fixes #1114 --- backend/src/controllers/sse.controller.ts | 5 +- backend/src/routes/health.routes.ts | 60 +++ backend/src/services/sorobanService.ts | 14 + backend/src/services/sse.service.ts | 59 ++- backend/tests/claimable.service.test.ts | 51 +++ backend/tests/sse.service.test.ts | 52 +++ docs/api/flowfi.postman_collection.json | 2 +- frontend/.gitignore | 3 + frontend/package.json | 4 +- frontend/src/lib/api-types.ts | 14 + package-lock.json | 463 ++++++++++++---------- 11 files changed, 512 insertions(+), 215 deletions(-) diff --git a/backend/src/controllers/sse.controller.ts b/backend/src/controllers/sse.controller.ts index 9b702257..8a094174 100644 --- a/backend/src/controllers/sse.controller.ts +++ b/backend/src/controllers/sse.controller.ts @@ -31,7 +31,8 @@ export const subscribe = async (req: Request, res: Response) => { try { const sourceIp = getClientIp(req); - const capacity = sseService.checkCapacity(sourceIp); + const authUserId = (req as AuthenticatedRequest).user?.publicKey; + const capacity = sseService.checkCapacity(sourceIp, authUserId); if (!capacity.allowed) { if (capacity.retryAfterSeconds) { res.setHeader('Retry-After', String(capacity.retryAfterSeconds)); @@ -87,7 +88,7 @@ export const subscribe = async (req: Request, res: Response) => { const requestId = requestContext.getStore()?.requestId; res.write(`data: ${JSON.stringify({ type: 'connected', clientId, requestId })}\n\n`); - sseService.addClient(clientId, res, subscriptions, sourceIp); + sseService.addClient(clientId, res, subscriptions, sourceIp, publicKey); return; } catch (error: unknown) { if (error instanceof z.ZodError) { diff --git a/backend/src/routes/health.routes.ts b/backend/src/routes/health.routes.ts index cf9e68dd..5b4e8ccb 100644 --- a/backend/src/routes/health.routes.ts +++ b/backend/src/routes/health.routes.ts @@ -1,6 +1,8 @@ import { Router, type Request, type Response } from 'express'; import { prisma } from '../lib/prisma.js'; import { INDEXER_STATE_ID } from '../lib/indexer-state.js'; +import { isRedisAvailable } from '../lib/redis.js'; +import { checkRpcHealth } from '../services/sorobanService.js'; const router = Router(); @@ -47,6 +49,39 @@ const router = Router(); * type: number * description: Server uptime in seconds * example: 3600 + * checks: + * type: object + * description: Per-subsystem status breakdown, so callers can tell "DB unreachable" apart from "indexer lagging" instead of inferring it from the top-level status alone. + * properties: + * database: + * type: object + * properties: + * status: + * type: string + * enum: [ok, down] + * indexer: + * type: object + * properties: + * status: + * type: string + * enum: [ok, degraded, disabled] + * enabled: + * type: boolean + * lagSeconds: + * type: integer + * nullable: true + * redis: + * type: object + * properties: + * status: + * type: string + * enum: [ok, unavailable, not_configured] + * sorobanRpc: + * type: object + * properties: + * status: + * type: string + * enum: [ok, down] * 503: * description: Service is degraded or unhealthy */ @@ -81,12 +116,37 @@ router.get('/', async (_req: Request, res: Response) => { const isHealthy = dbStatus === 'connected' && !indexerDegraded; const status = isHealthy ? 'ok' : 'degraded'; + // Redis is optional (single-instance SSE mode falls back gracefully when it's + // absent), so its status never affects the top-level `isHealthy` verdict. + const redisConfigured = !!process.env.REDIS_URL; + const redisStatus = !redisConfigured ? 'not_configured' : isRedisAvailable() ? 'ok' : 'unavailable'; + + // Soroban RPC reachability is reported for observability only — it does not + // gate liveness, since a transient RPC blip shouldn't take the service down. + const sorobanRpcOk = await checkRpcHealth(); + res.status(isHealthy ? 200 : 503).json({ status, db: dbStatus, indexerEnabled, indexerLag: indexerLag === -1 ? null : indexerLag, uptime: process.uptime(), + checks: { + database: { + status: dbStatus === 'connected' ? 'ok' : 'down', + }, + indexer: { + status: !indexerEnabled ? 'disabled' : indexerDegraded ? 'degraded' : 'ok', + enabled: indexerEnabled, + lagSeconds: indexerLag === -1 ? null : indexerLag, + }, + redis: { + status: redisStatus, + }, + sorobanRpc: { + status: sorobanRpcOk ? 'ok' : 'down', + }, + }, }); }); diff --git a/backend/src/services/sorobanService.ts b/backend/src/services/sorobanService.ts index d13b5843..987521f1 100644 --- a/backend/src/services/sorobanService.ts +++ b/backend/src/services/sorobanService.ts @@ -126,6 +126,20 @@ function getServer(): rpc.Server { return _server; } +/** + * Lightweight connectivity check used by the /health endpoint. + * Calls the RPC server's getHealth() with a bounded timeout so a slow or + * unreachable Soroban RPC endpoint can't hang the health check. + */ +export async function checkRpcHealth(timeoutMs = 3_000): Promise { + try { + await withRpcTimeout('soroban rpc health check', () => getServer().getHealth(), timeoutMs); + return true; + } catch { + return false; + } +} + export function setServer(server: rpc.Server): void { _server = server; } diff --git a/backend/src/services/sse.service.ts b/backend/src/services/sse.service.ts index 88401480..acf9c50b 100644 --- a/backend/src/services/sse.service.ts +++ b/backend/src/services/sse.service.ts @@ -5,6 +5,7 @@ import { isRedisAvailable, getPublisher, getSubscriber } from '../lib/redis.js'; const HEARTBEAT_INTERVAL_MS = 30_000; const MAX_WRITABLE_BUFFER = 64 * 1024; const MAX_CONNECTIONS_PER_IP = 5; +const MAX_CONNECTIONS_PER_USER = 10; const RETRY_AFTER_SECONDS = 60; interface SSEClient { @@ -13,6 +14,7 @@ interface SSEClient { subscriptions: Set; paused: boolean; ip: string; + userId?: string; } interface SSECapacityCheckResult { @@ -27,8 +29,10 @@ export class SSEService { private heartbeatTimer: ReturnType | null = null; private slowClientsDropped = 0; private readonly ipConnectionCounts: Map = new Map(); + private readonly userConnectionCounts: Map = new Map(); private shuttingDown = false; private perIpPeakConnections = 0; + private perUserPeakConnections = 0; private readonly maxConnections: number = (() => { const parsed = Number.parseInt(process.env.MAX_SSE_CONNECTIONS ?? '10000', 10); @@ -61,7 +65,7 @@ export class SSEService { logger.info('[SSEService] Redis pub/sub subscription active.'); } - checkCapacity(ip: string): SSECapacityCheckResult { + checkCapacity(ip: string, userId?: string): SSECapacityCheckResult { if (this.clients.size >= this.maxConnections) { return { allowed: false, @@ -80,25 +84,53 @@ export class SSEService { }; } + // Independent of the per-IP cap: bounds how many concurrent SSE + // subscriptions a single authenticated user can hold regardless of which + // IP(s) they connect from (e.g. multiple tabs/devices behind different NATs). + if (userId) { + const currentUserConnections = this.userConnectionCounts.get(userId) ?? 0; + if (currentUserConnections >= MAX_CONNECTIONS_PER_USER) { + return { + allowed: false, + status: 429, + retryAfterSeconds: RETRY_AFTER_SECONDS, + message: `Too many concurrent SSE connections for this user. Max ${MAX_CONNECTIONS_PER_USER}.`, + }; + } + } + return { allowed: true }; } - addClient(clientId: string, res: Response, subscriptions: string[] = [], ip = 'unknown'): void { + addClient( + clientId: string, + res: Response, + subscriptions: string[] = [], + ip = 'unknown', + userId?: string, + ): void { const nextIpCount = (this.ipConnectionCounts.get(ip) ?? 0) + 1; this.ipConnectionCounts.set(ip, nextIpCount); this.perIpPeakConnections = Math.max(this.perIpPeakConnections, nextIpCount); + if (userId) { + const nextUserCount = (this.userConnectionCounts.get(userId) ?? 0) + 1; + this.userConnectionCounts.set(userId, nextUserCount); + this.perUserPeakConnections = Math.max(this.perUserPeakConnections, nextUserCount); + } + const client: SSEClient = { id: clientId, res, subscriptions: new Set(subscriptions), paused: false, ip, + ...(userId !== undefined && { userId }), }; this.clients.set(clientId, client); logger.info( - `[SSEService] Connection opened: ${clientId}, ip: ${ip}, subscriptions: ${subscriptions.join(', ')}` + `[SSEService] Connection opened: ${clientId}, ip: ${ip}, userId: ${userId ?? 'n/a'}, subscriptions: ${subscriptions.join(', ')}` ); res.on('close', () => { @@ -194,6 +226,18 @@ export class SSEService { return this.ipConnectionCounts.size; } + getPerUserPeakConnections(): number { + return this.perUserPeakConnections; + } + + getActiveUserCount(): number { + return this.userConnectionCounts.size; + } + + getUserConnectionCount(userId: string): number { + return this.userConnectionCounts.get(userId) ?? 0; + } + stopHeartbeat(): void { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); @@ -235,6 +279,15 @@ export class SSEService { this.ipConnectionCounts.set(client.ip, currentIpCount - 1); } + if (client.userId) { + const currentUserCount = this.userConnectionCounts.get(client.userId) ?? 0; + if (currentUserCount <= 1) { + this.userConnectionCounts.delete(client.userId); + } else { + this.userConnectionCounts.set(client.userId, currentUserCount - 1); + } + } + try { if (!client.res.writableEnded) { client.res.end(); diff --git a/backend/tests/claimable.service.test.ts b/backend/tests/claimable.service.test.ts index c4628b5a..5b0bb8e2 100644 --- a/backend/tests/claimable.service.test.ts +++ b/backend/tests/claimable.service.test.ts @@ -131,6 +131,57 @@ describe('ClaimableAmountService', () => { expect(third.cached).toBe(false); }); + it('reflects an indexed withdrawal immediately, without waiting for the cache TTL', () => { + // The cache key is derived from getStateFingerprint(), which folds in + // withdrawnAmount and lastUpdateTime (see claimable.service.ts). When the + // indexer processes a TokensWithdrawn event it updates those fields on the + // Stream row, so the *next* read (which is always given the freshly + // reloaded stream from the DB, see stream.controller.ts / withdraw.ts) + // naturally lands on a different cache key and can never return the + // pre-withdrawal cached value — invalidation falls out of the key design + // rather than needing an explicit "on withdrawal, delete this key" hook. + vi.setSystemTime(50_000); + const service = new ClaimableAmountService({ + cacheTtlMs: 60_000, // deliberately long TTL to prove this isn't just a TTL expiry + }); + + const preWithdrawalState = makeStreamState({ + streamId: 7, + ratePerSecond: '10', + depositedAmount: '1000', + withdrawnAmount: '0', + lastUpdateTime: 0, + }); + + // Prime the cache with the pre-withdrawal state. + const primed = service.getClaimableAmount(preWithdrawalState, 40); + expect(primed.cached).toBe(false); + expect(primed.claimableAmount).toBe('400'); // 40s * 10/s + + // A repeated read with the identical state still hits the cache. + const repeated = service.getClaimableAmount(preWithdrawalState, 40); + expect(repeated.cached).toBe(true); + + // Simulate the indexer processing a TokensWithdrawn event: withdrawnAmount + // and lastUpdateTime are advanced on the stream row, exactly as + // handleTokensWithdrawn does in soroban-event-worker.ts. + const postWithdrawalState = makeStreamState({ + streamId: 7, + ratePerSecond: '10', + depositedAmount: '1000', + withdrawnAmount: '400', + lastUpdateTime: 40, + }); + + // Well within the 60s TTL, so this only passes if the state change (not + // TTL expiry) is what causes the fresh calculation. + vi.advanceTimersByTime(1_000); + const afterWithdrawal = service.getClaimableAmount(postWithdrawalState, 40); + + expect(afterWithdrawal.cached).toBe(false); + expect(afterWithdrawal.claimableAmount).toBe('0'); // fully withdrawn as of lastUpdateTime=40 + }); + it('caps multiplication overflow at the remaining balance', () => { const i128Max = ((1n << 127n) - 1n).toString(); vi.setSystemTime(1_000_000); diff --git a/backend/tests/sse.service.test.ts b/backend/tests/sse.service.test.ts index fcf287dd..7a86dca2 100644 --- a/backend/tests/sse.service.test.ts +++ b/backend/tests/sse.service.test.ts @@ -98,3 +98,55 @@ describe('SSEService backpressure', () => { expect(service.getSlowClientsDropped()).toBe(1); }); }); + +describe('SSEService per-user connection cap', () => { + let service: SSEService; + + afterEach(() => { + service.stopHeartbeat(); + }); + + it('allows connections from a user up to the per-user cap, independent of IP', () => { + service = new SSEService(); + const userId = 'GUSER123'; + + // Spread the connections across different IPs so only the per-user cap + // (not the pre-existing per-IP cap) is exercised here. + for (let i = 0; i < 10; i += 1) { + const capacity = service.checkCapacity(`10.0.0.${i}`, userId); + expect(capacity.allowed).toBe(true); + service.addClient(`client-${i}`, createMockResponse(), [], `10.0.0.${i}`, userId); + } + + expect(service.getUserConnectionCount(userId)).toBe(10); + + // The 11th connection for the same user, from yet another IP, must be rejected. + const rejected = service.checkCapacity('10.0.0.99', userId); + expect(rejected.allowed).toBe(false); + expect(rejected.status).toBe(429); + expect(rejected.message).toMatch(/user/i); + expect(rejected.retryAfterSeconds).toBeGreaterThan(0); + }); + + it('does not cap unauthenticated/anonymous checks that omit a userId', () => { + service = new SSEService(); + + for (let i = 0; i < 10; i += 1) { + const capacity = service.checkCapacity(`10.1.0.${i}`); + expect(capacity.allowed).toBe(true); + } + }); + + it('releases the per-user slot once a client disconnects', () => { + service = new SSEService(); + const userId = 'GUSER456'; + + const res = createMockResponse(); + service.addClient('client-a', res, [], '10.2.0.1', userId); + expect(service.getUserConnectionCount(userId)).toBe(1); + + res.emitter.emit('close'); + + expect(service.getUserConnectionCount(userId)).toBe(0); + }); +}); diff --git a/docs/api/flowfi.postman_collection.json b/docs/api/flowfi.postman_collection.json index 15cfdff0..f1aa3675 100644 --- a/docs/api/flowfi.postman_collection.json +++ b/docs/api/flowfi.postman_collection.json @@ -60,7 +60,7 @@ "code": 200, "_postman_previewlanguage": "json", "header": [{ "key": "Content-Type", "value": "application/json; charset=utf-8" }], - "body": "{\n \"status\": \"healthy\",\n \"timestamp\": \"2024-02-21T14:30:00.000Z\",\n \"uptime\": 3600,\n \"version\": \"1.0.0\",\n \"api\": {\n \"supported\": [\"v1\"],\n \"default\": \"v1\"\n }\n}" + "body": "{\n \"status\": \"ok\",\n \"db\": \"connected\",\n \"indexerEnabled\": true,\n \"indexerLag\": 5,\n \"uptime\": 3600,\n \"checks\": {\n \"database\": { \"status\": \"ok\" },\n \"indexer\": { \"status\": \"ok\", \"enabled\": true, \"lagSeconds\": 5 },\n \"redis\": { \"status\": \"ok\" },\n \"sorobanRpc\": { \"status\": \"ok\" }\n }\n}" } ] }, diff --git a/frontend/.gitignore b/frontend/.gitignore index 193edd49..49a317f1 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -41,3 +41,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# manually-triggered API type codegen output (see src/lib/api-types.ts) +src/lib/api-types.generated.ts diff --git a/frontend/package.json b/frontend/package.json index 577e543b..4e359d50 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,8 @@ "lint": "eslint", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "codegen:api-types": "openapi-typescript http://localhost:3001/api-docs.json -o src/lib/api-types.generated.ts" }, "dependencies": { "@stellar/freighter-api": "^6.0.1", @@ -38,6 +39,7 @@ "eslint-config-next": "^16.2.9", "happy-dom": "^20.10.3", "jsdom": "^27.0.1", + "openapi-typescript": "^7.13.0", "tailwindcss": "^4", "typescript": "^5", "vitest": "^2.1.9" diff --git a/frontend/src/lib/api-types.ts b/frontend/src/lib/api-types.ts index 349628b5..0ff354c2 100644 --- a/frontend/src/lib/api-types.ts +++ b/frontend/src/lib/api-types.ts @@ -1,3 +1,17 @@ +/** + * NOTE: These types are hand-written and must be manually kept in sync with + * the backend's OpenAPI/swagger source of truth: + * backend/src/config/swagger.ts (JSDoc annotations on the route files feed + * swagger-jsdoc, which builds the spec served at /api-docs.json). + * + * An opt-in codegen script exists to help cross-check this file against the + * live spec without replacing it automatically: + * `npm run codegen:api-types` (in frontend/) runs openapi-typescript + * against a locally running backend's /api-docs.json and writes the result + * to src/lib/api-types.generated.ts. It is NOT wired into CI and does NOT + * replace these hand-written types/call sites — run it manually, diff the + * output against this file, and update this file by hand when they drift. + */ export interface BackendUser { id: string; publicKey: string; diff --git a/package-lock.json b/package-lock.json index 6f78cc24..b2afa1fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -135,6 +136,7 @@ "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.7", @@ -258,6 +260,7 @@ "eslint-config-next": "^16.2.9", "happy-dom": "^20.10.3", "jsdom": "^27.0.1", + "openapi-typescript": "^7.13.0", "tailwindcss": "^4", "typescript": "^5", "vitest": "^2.1.9" @@ -295,7 +298,6 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -476,6 +478,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -593,6 +596,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -698,7 +702,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -708,7 +712,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -742,7 +746,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -802,7 +806,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -980,6 +984,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1028,6 +1033,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1048,7 +1054,8 @@ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@electric-sql/pglite-socket": { "version": "0.0.20", @@ -1073,39 +1080,6 @@ "@electric-sql/pglite": "0.3.15" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -2398,9 +2372,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2417,9 +2388,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2436,9 +2404,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2455,9 +2420,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2580,7 +2542,6 @@ "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/Boshen" } @@ -2798,6 +2759,82 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.18", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.18.tgz", + "integrity": "sha512-UyKIm0wTPw5BcY7Z2PkbK1Ma260um96LSBWXHrdSMe+ZV0EPMyDfAcUcjjm3qEiGST9OK/1TriekdPCZkn4Q3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.3.0", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.3.tgz", + "integrity": "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", @@ -2811,7 +2848,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -2829,7 +2865,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -2847,7 +2882,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -2865,7 +2899,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -2883,7 +2916,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -2896,15 +2928,11 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -2917,15 +2945,11 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -2938,15 +2962,11 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -2959,15 +2979,11 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -2980,15 +2996,11 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -3001,15 +3013,11 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -3027,7 +3035,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -3042,7 +3049,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", @@ -3059,7 +3065,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@tybys/wasm-util": "^0.10.3" }, @@ -3085,7 +3090,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -3103,7 +3107,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -3207,9 +3210,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3224,9 +3224,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3241,9 +3238,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3258,9 +3252,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3275,9 +3266,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3292,9 +3280,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3309,9 +3294,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3326,9 +3308,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3343,9 +3322,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3360,9 +3336,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3377,9 +3350,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3394,9 +3364,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3411,9 +3378,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3755,9 +3719,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3779,9 +3740,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3803,9 +3761,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3976,9 +3931,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3996,9 +3948,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4015,9 +3964,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4035,9 +3981,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4077,27 +4020,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "dev": true, @@ -4223,6 +4145,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4489,6 +4412,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4530,6 +4454,7 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4540,6 +4465,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4675,6 +4601,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -5401,6 +5328,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5458,6 +5386,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -5957,6 +5895,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6216,6 +6155,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -6379,6 +6325,13 @@ "node": ">=12.20" } }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6577,7 +6530,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -7155,6 +7109,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7225,6 +7180,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7410,6 +7366,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7691,6 +7648,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -8550,6 +8508,7 @@ "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -8735,6 +8694,19 @@ "node": ">=8" } }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -9385,6 +9357,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9583,7 +9565,6 @@ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -9621,7 +9602,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9683,7 +9663,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9705,7 +9684,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9722,15 +9700,11 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9747,15 +9721,11 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9771,9 +9741,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9795,15 +9762,11 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9825,7 +9788,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9847,7 +9809,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9869,7 +9830,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -9886,15 +9846,11 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -10089,7 +10045,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.25.4", @@ -10802,6 +10758,40 @@ "license": "MIT", "peer": true }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10889,6 +10879,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -11014,6 +11022,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", @@ -11138,6 +11147,16 @@ "pathe": "^2.0.3" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -11291,6 +11310,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "7.4.1", "@prisma/dev": "0.20.0", @@ -11490,6 +11510,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11499,6 +11520,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11730,7 +11752,6 @@ "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" @@ -13081,6 +13102,7 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13320,6 +13342,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -13347,6 +13370,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -13444,6 +13480,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13594,6 +13631,13 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, "node_modules/urijs": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", @@ -13643,6 +13687,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -14156,6 +14201,7 @@ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", @@ -14728,21 +14774,21 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", - "optional": true, - "bin": { - "yaml": "bin.mjs" - }, "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" + "node": ">=12" } }, "node_modules/yn": { @@ -14784,6 +14830,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }