From 3115da5afb4ae221ed97bcf70659ac528ea0e044 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:28:20 +0000 Subject: [PATCH 1/2] Migrate server backend from Express to Next.js Co-Authored-By: Nader Dabit --- README.md | 14 +- server/.gitignore | 5 +- server/__tests__/api.test.ts | 141 ++ server/app/chat/[provider]/route.ts | 47 + server/app/health/route.ts | 5 + server/app/images/[provider]/route.ts | 53 + server/app/layout.tsx | 14 + server/app/models/route.ts | 19 + server/app/page.tsx | 22 + server/lib/auth.ts | 21 + server/{src => lib}/chat/claude.ts | 27 +- server/{src => lib}/chat/gemini.ts | 27 +- server/lib/chat/glm.ts | 11 + server/lib/chat/gpt.ts | 11 + server/lib/chat/index.ts | 16 + server/lib/chat/kimi.ts | 11 + server/{src => lib}/chat/openaiCompatible.ts | 30 +- server/{src => lib}/images/gemini.ts | 42 +- server/{src => lib}/models.ts | 0 server/lib/rateLimit.ts | 56 + server/lib/sse.ts | 74 + server/{src => lib}/types.ts | 0 server/next.config.ts | 10 + server/package.json | 40 +- server/pnpm-lock.yaml | 1938 ++++++------------ server/proxy.ts | 25 + server/src/__tests__/api.test.ts | 113 - server/src/chat/chatRouter.ts | 20 - server/src/chat/glm.ts | 13 - server/src/chat/gpt.ts | 13 - server/src/chat/kimi.ts | 13 - server/src/images/imagesRouter.ts | 10 - server/src/index.ts | 50 - server/src/middleware.ts | 35 - server/src/sse.ts | 55 - server/tsconfig.json | 145 +- server/vitest.config.ts | 14 + 37 files changed, 1240 insertions(+), 1900 deletions(-) create mode 100644 server/__tests__/api.test.ts create mode 100644 server/app/chat/[provider]/route.ts create mode 100644 server/app/health/route.ts create mode 100644 server/app/images/[provider]/route.ts create mode 100644 server/app/layout.tsx create mode 100644 server/app/models/route.ts create mode 100644 server/app/page.tsx create mode 100644 server/lib/auth.ts rename server/{src => lib}/chat/claude.ts (71%) rename server/{src => lib}/chat/gemini.ts (71%) create mode 100644 server/lib/chat/glm.ts create mode 100644 server/lib/chat/gpt.ts create mode 100644 server/lib/chat/index.ts create mode 100644 server/lib/chat/kimi.ts rename server/{src => lib}/chat/openaiCompatible.ts (67%) rename server/{src => lib}/images/gemini.ts (63%) rename server/{src => lib}/models.ts (100%) create mode 100644 server/lib/rateLimit.ts create mode 100644 server/lib/sse.ts rename server/{src => lib}/types.ts (100%) create mode 100644 server/next.config.ts create mode 100644 server/proxy.ts delete mode 100644 server/src/__tests__/api.test.ts delete mode 100644 server/src/chat/chatRouter.ts delete mode 100644 server/src/chat/glm.ts delete mode 100644 server/src/chat/gpt.ts delete mode 100644 server/src/chat/kimi.ts delete mode 100644 server/src/images/imagesRouter.ts delete mode 100644 server/src/index.ts delete mode 100644 server/src/middleware.ts delete mode 100644 server/src/sse.ts create mode 100644 server/vitest.config.ts diff --git a/README.md b/README.md index 657e16a2..9b7bcc1d 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,14 @@ npm start ### Running the server -Change into the server directory and run: +The server is a [Next.js](https://nextjs.org/) app (App Router) that proxies requests to the AI providers. Change into the server directory and run: ```sh npm run dev ``` +It starts on port `3050` (matching the bundled `ngrok` scripts). Use `npm run build && npm start` for a production server. + ### Environment variables The server environment variables are available in `server/.env.example`. If already not present, update this file name to `.env` and configure server environment variables. Gemini image generation requires `GEMINI_API_KEY`. @@ -108,9 +110,9 @@ For adding models, once the model definition is added to the `MODELS` array, you ### On the server -Create a new file in the `server/src/chat` folder that corresponds to the model type you created in the mobile app. You can probably copy and re-use a lot of the streaming code from the other existing paths to get you started. +Create a new file in the `server/lib/chat` folder that corresponds to the model type you created in the mobile app. Each handler is a function that takes the parsed `ChatRequest` and returns a streaming SSE `Response` (see `sseResponse` in `server/lib/sse.ts`). You can copy and re-use a lot of the streaming code from the other existing handlers to get you started. -Next, update `server/src/chat/chatRouter` to use the new route. +Next, register the handler in `server/lib/chat/index.ts` (the `CHAT_HANDLERS` map). The map key becomes the path segment, e.g. a `myModel` key is served at `POST /chat/myModel` by `server/app/chat/[provider]/route.ts`. ## Configuring Image Models @@ -132,10 +134,10 @@ The app is configured to handle both, but you must update the `generate` functio #### Gemini (Nano Banana) -Gemini image generation is handled in `server/src/images/gemini.ts`. Configure `GEMINI_API_KEY` and select the Nano Banana image models from the settings screen. +Gemini image generation is handled in `server/lib/images/gemini.ts`. Configure `GEMINI_API_KEY` and select the Nano Banana image models from the settings screen. #### Other API providers -Create a new file in `server/src/images/modelName`, update the handler function to handle the new API call. +Create a new file in `server/lib/images/modelName`, exporting a handler that returns a JSON `Response`. -Next, update `server/src/images/imagesRouter` to use the new route. \ No newline at end of file +Next, register it in the `IMAGE_HANDLERS` map in `server/app/images/[provider]/route.ts` so it is served at `POST /images/modelName`. \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore index f7f4bbe9..29369c3c 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,5 +1,8 @@ node_modules .env +.next +next-env.d.ts +*.tsbuildinfo dist yarn.lock -package-lock.json \ No newline at end of file +package-lock.json diff --git a/server/__tests__/api.test.ts b/server/__tests__/api.test.ts new file mode 100644 index 00000000..7ec8204b --- /dev/null +++ b/server/__tests__/api.test.ts @@ -0,0 +1,141 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { GET as healthGET } from '@/app/health/route' +import { GET as modelsGET } from '@/app/models/route' +import { POST as chatPOST } from '@/app/chat/[provider]/route' + +function chatRequest(body: unknown, headers: Record = {}) { + return new Request('http://localhost/chat/x', { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...headers }, + body: JSON.stringify(body) + }) +} + +function params(provider: string) { + return { params: Promise.resolve({ provider }) } +} + +function sseResponse(events: string[]) { + const encoder = new TextEncoder() + const stream = new ReadableStream({ + start(controller) { + for (const event of events) { + controller.enqueue(encoder.encode(event)) + } + controller.close() + } + }) + return new Response(stream, { + status: 200, + headers: { 'Content-Type': 'text/event-stream' } + }) +} + +describe('server', () => { + beforeEach(() => { + delete process.env.API_AUTH_TOKEN + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('GET /health returns ok', async () => { + const res = healthGET() + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ status: 'ok' }) + }) + + it('GET /models returns chat and image models', async () => { + const res = modelsGET() + expect(res.status).toBe(200) + const body = await res.json() + expect(body.chatModels.length).toBeGreaterThan(0) + expect(body.imageModels.length).toBeGreaterThan(0) + const claude = body.chatModels.find((m: any) => m.label === 'claudeOpus') + expect(claude.provider).toBe('anthropic') + expect(body.chatModels.every((m: any) => !m.modelId)).toBe(true) + }) + + it('rejects invalid chat request bodies', async () => { + const res = await chatPOST(chatRequest({ model: 'claudeOpus' }), params('claude')) + expect(res.status).toBe(400) + expect((await res.json()).error).toBe('invalid request body') + }) + + it('returns 404 for unknown chat providers', async () => { + const res = await chatPOST( + chatRequest({ model: 'claudeOpus', messages: [{ role: 'user', content: 'hi' }] }), + params('nope') + ) + expect(res.status).toBe(404) + }) + + it('requires bearer token when API_AUTH_TOKEN is set', async () => { + process.env.API_AUTH_TOKEN = 'secret' + const res = await chatPOST( + chatRequest({ model: 'claudeOpus', messages: [{ role: 'user', content: 'hi' }] }), + params('claude') + ) + expect(res.status).toBe(401) + }) + + it('streams claude tokens as normalized SSE events', async () => { + vi.spyOn(global, 'fetch').mockResolvedValue( + sseResponse([ + 'data: {"type":"content_block_delta","delta":{"text":"Hel"}}\n\n', + 'data: {"type":"content_block_delta","del', + 'ta":{"text":"lo"}}\n\n' + ]) as any + ) + const res = await chatPOST( + chatRequest({ model: 'claudeOpus', messages: [{ role: 'user', content: 'hi' }] }), + params('claude') + ) + expect(res.status).toBe(200) + const text = await res.text() + expect(text).toContain('data: {"content":"Hel"}') + expect(text).toContain('data: {"content":"lo"}') + expect(text).toContain('data: [DONE]') + }) + + it('streams openai-compatible tokens as normalized SSE events', async () => { + vi.spyOn(global, 'fetch').mockResolvedValue( + sseResponse([ + 'data: {"choices":[{"delta":{"content":"Hi"}}]}\n\n', + 'data: [DONE]\n\n' + ]) as any + ) + const res = await chatPOST( + chatRequest({ model: 'gpt52', messages: [{ role: 'user', content: 'hi' }] }), + params('gpt') + ) + expect(res.status).toBe(200) + const text = await res.text() + expect(text).toContain('data: {"content":"Hi"}') + expect(text).toContain('data: [DONE]') + }) + + it('emits an error event for unsupported models', async () => { + const res = await chatPOST( + chatRequest({ model: 'not-a-model', messages: [{ role: 'user', content: 'hi' }] }), + params('gpt') + ) + const text = await res.text() + expect(text).toContain('unsupported model') + expect(text).toContain('data: [DONE]') + }) + + it('emits an error event when the provider fails', async () => { + vi.spyOn(global, 'fetch').mockResolvedValue( + new Response('rate limited', { status: 429 }) as any + ) + const res = await chatPOST( + chatRequest({ model: 'claudeOpus', messages: [{ role: 'user', content: 'hi' }] }), + params('claude') + ) + const text = await res.text() + expect(text).toContain('provider error (429)') + expect(text).toContain('data: [DONE]') + }) +}) diff --git a/server/app/chat/[provider]/route.ts b/server/app/chat/[provider]/route.ts new file mode 100644 index 00000000..ff9a8367 --- /dev/null +++ b/server/app/chat/[provider]/route.ts @@ -0,0 +1,47 @@ +import { checkAuth } from '@/lib/auth' +import { checkRateLimit } from '@/lib/rateLimit' +import { chatRequestSchema } from '@/lib/types' +import { CHAT_HANDLERS } from '@/lib/chat' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function POST( + req: Request, + ctx: { params: Promise<{ provider: string }> } +) { + const authError = checkAuth(req) + if (authError) return authError + + const limited = checkRateLimit(req) + if (limited) return limited + + const { provider } = await ctx.params + const handler = CHAT_HANDLERS[provider] + if (!handler) { + return Response.json({ error: `unknown chat provider: ${provider}` }, { status: 404 }) + } + + let json: unknown + try { + json = await req.json() + } catch { + return Response.json( + { error: 'invalid request body', details: ['body must be valid JSON'] }, + { status: 400 } + ) + } + + const result = chatRequestSchema.safeParse(json) + if (!result.success) { + return Response.json( + { + error: 'invalid request body', + details: result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`) + }, + { status: 400 } + ) + } + + return handler(result.data) +} diff --git a/server/app/health/route.ts b/server/app/health/route.ts new file mode 100644 index 00000000..b8788a7a --- /dev/null +++ b/server/app/health/route.ts @@ -0,0 +1,5 @@ +export const dynamic = 'force-dynamic' + +export function GET() { + return Response.json({ status: 'ok' }) +} diff --git a/server/app/images/[provider]/route.ts b/server/app/images/[provider]/route.ts new file mode 100644 index 00000000..689a551a --- /dev/null +++ b/server/app/images/[provider]/route.ts @@ -0,0 +1,53 @@ +import { checkAuth } from '@/lib/auth' +import { checkRateLimit } from '@/lib/rateLimit' +import { geminiImage } from '@/lib/images/gemini' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +const IMAGE_HANDLERS: Record = { + gemini: geminiImage +} + +export async function POST( + req: Request, + ctx: { params: Promise<{ provider: string }> } +) { + const authError = checkAuth(req) + if (authError) return authError + + const limited = checkRateLimit(req) + if (limited) return limited + + const { provider } = await ctx.params + const handler = IMAGE_HANDLERS[provider] + if (!handler) { + return Response.json({ error: `unknown image provider: ${provider}` }, { status: 404 }) + } + + const contentType = req.headers.get('content-type') || '' + + let model = '' + let prompt: string | undefined + let file: { mimeType: string; data: string } | undefined + + if (contentType.includes('multipart/form-data')) { + const form = await req.formData() + model = form.get('model')?.toString() || '' + prompt = form.get('prompt')?.toString() || undefined + const uploaded = form.get('file') + if (uploaded instanceof File) { + const buffer = Buffer.from(await uploaded.arrayBuffer()) + file = { + mimeType: uploaded.type || 'application/octet-stream', + data: buffer.toString('base64') + } + } + } else { + const json = await req.json().catch(() => ({}) as any) + model = typeof json.model === 'string' ? json.model : '' + prompt = typeof json.prompt === 'string' ? json.prompt : undefined + } + + return handler({ model, prompt, file }) +} diff --git a/server/app/layout.tsx b/server/app/layout.tsx new file mode 100644 index 00000000..9ecf5f82 --- /dev/null +++ b/server/app/layout.tsx @@ -0,0 +1,14 @@ +import type { ReactNode } from 'react' + +export const metadata = { + title: 'React Native AI Server', + description: 'Next.js backend proxy for React Native AI' +} + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/server/app/models/route.ts b/server/app/models/route.ts new file mode 100644 index 00000000..5d7052e9 --- /dev/null +++ b/server/app/models/route.ts @@ -0,0 +1,19 @@ +import { CHAT_MODELS, IMAGE_MODELS } from '@/lib/models' + +export const dynamic = 'force-dynamic' + +export function GET() { + return Response.json({ + chatModels: Object.values(CHAT_MODELS).map(({ name, label, provider, supportsVision }) => ({ + name, + label, + provider, + supportsVision + })), + imageModels: Object.values(IMAGE_MODELS).map(({ name, label, provider }) => ({ + name, + label, + provider + })) + }) +} diff --git a/server/app/page.tsx b/server/app/page.tsx new file mode 100644 index 00000000..070783ef --- /dev/null +++ b/server/app/page.tsx @@ -0,0 +1,22 @@ +export default function Home() { + return ( +
+

React Native AI Server

+

The backend proxy is running.

+
    +
  • + GET /health +
  • +
  • + GET /models +
  • +
  • + POST /chat/:provider (claude, gpt, gemini, glm, kimi) +
  • +
  • + POST /images/:provider (gemini) +
  • +
+
+ ) +} diff --git a/server/lib/auth.ts b/server/lib/auth.ts new file mode 100644 index 00000000..d345c16c --- /dev/null +++ b/server/lib/auth.ts @@ -0,0 +1,21 @@ +import { timingSafeEqual } from 'crypto' + +/** + * Optional bearer-token auth. Enabled by setting API_AUTH_TOKEN in the + * server environment; clients must then send `Authorization: Bearer `. + * Returns a 401 Response when the check fails, otherwise null. + */ +export function checkAuth(req: Request): Response | null { + const token = process.env.API_AUTH_TOKEN + if (!token) return null + + const header = req.headers.get('authorization') || '' + const expected = Buffer.from(`Bearer ${token}`) + const provided = Buffer.from(header) + + if (expected.length === provided.length && timingSafeEqual(expected, provided)) { + return null + } + + return Response.json({ error: 'unauthorized' }, { status: 401 }) +} diff --git a/server/src/chat/claude.ts b/server/lib/chat/claude.ts similarity index 71% rename from server/src/chat/claude.ts rename to server/lib/chat/claude.ts index 54fe0f59..1d888d74 100644 --- a/server/src/chat/claude.ts +++ b/server/lib/chat/claude.ts @@ -1,8 +1,6 @@ -import { Request, Response } from 'express' -import asyncHandler from 'express-async-handler' import { getChatModel } from '../models' import { ChatMessage, ChatRequest } from '../types' -import { initSSE, sendToken, sendError, sendDone, createSSEParser, pumpStream } from '../sse' +import { sseResponse, createSSEParser, pumpStream } from '../sse' function toAnthropicMessages(messages: ChatMessage[]) { return messages @@ -28,15 +26,12 @@ function toAnthropicMessages(messages: ChatMessage[]) { }) } -export const claude = asyncHandler(async (req: Request, res: Response) => { - initSSE(res) - try { - const { messages, model }: ChatRequest = req.body +export function claude({ messages, model }: ChatRequest): Response { + return sseResponse(async writer => { const chatModel = getChatModel(model) if (!chatModel || chatModel.provider !== 'anthropic') { - sendError(res, `unsupported model: ${model}`) - sendDone(res) + writer.sendError(`unsupported model: ${model}`) return } @@ -61,8 +56,7 @@ export const claude = asyncHandler(async (req: Request, res: Response) => { if (!response.ok) { const detail = await response.text() console.error('anthropic error:', response.status, detail) - sendError(res, `provider error (${response.status})`) - sendDone(res) + writer.sendError(`provider error (${response.status})`) return } @@ -70,16 +64,11 @@ export const claude = asyncHandler(async (req: Request, res: Response) => { try { const parsed = JSON.parse(data) if (parsed.type === 'content_block_delta' && parsed.delta?.text) { - sendToken(res, parsed.delta.text) + writer.sendToken(parsed.delta.text) } } catch {} }) await pumpStream(response.body, parse) - sendDone(res) - } catch (err) { - console.error('error in claude chat:', err) - sendError(res, 'unexpected server error') - sendDone(res) - } -}) + }) +} diff --git a/server/src/chat/gemini.ts b/server/lib/chat/gemini.ts similarity index 71% rename from server/src/chat/gemini.ts rename to server/lib/chat/gemini.ts index 16519b16..264a456e 100644 --- a/server/src/chat/gemini.ts +++ b/server/lib/chat/gemini.ts @@ -1,8 +1,6 @@ -import { Request, Response } from 'express' -import asyncHandler from 'express-async-handler' import { getChatModel } from '../models' import { ChatMessage, ChatRequest } from '../types' -import { initSSE, sendToken, sendError, sendDone, createSSEParser, pumpStream } from '../sse' +import { sseResponse, createSSEParser, pumpStream } from '../sse' function toGeminiContents(messages: ChatMessage[]) { return messages @@ -25,15 +23,12 @@ function toGeminiContents(messages: ChatMessage[]) { }) } -export const gemini = asyncHandler(async (req: Request, res: Response) => { - initSSE(res) - try { - const { messages, model }: ChatRequest = req.body +export function gemini({ messages, model }: ChatRequest): Response { + return sseResponse(async writer => { const chatModel = getChatModel(model) if (!chatModel || chatModel.provider !== 'google') { - sendError(res, `unsupported model: ${model}`) - sendDone(res) + writer.sendError(`unsupported model: ${model}`) return } @@ -57,8 +52,7 @@ export const gemini = asyncHandler(async (req: Request, res: Response) => { if (!response.ok) { const detail = await response.text() console.error('gemini error:', response.status, detail) - sendError(res, `provider error (${response.status})`) - sendDone(res) + writer.sendError(`provider error (${response.status})`) return } @@ -68,15 +62,10 @@ export const gemini = asyncHandler(async (req: Request, res: Response) => { const text = parsed.candidates?.[0]?.content?.parts ?.map((p: any) => p.text || '') .join('') - if (text) sendToken(res, text) + if (text) writer.sendToken(text) } catch {} }) await pumpStream(response.body, parse) - sendDone(res) - } catch (err) { - console.error('error in gemini chat:', err) - sendError(res, 'unexpected server error') - sendDone(res) - } -}) + }) +} diff --git a/server/lib/chat/glm.ts b/server/lib/chat/glm.ts new file mode 100644 index 00000000..1b64b178 --- /dev/null +++ b/server/lib/chat/glm.ts @@ -0,0 +1,11 @@ +import { ChatRequest } from '../types' +import { streamOpenAICompatible } from './openaiCompatible' + +export function glm(body: ChatRequest): Response { + return streamOpenAICompatible({ + body, + provider: 'zai', + apiUrl: 'https://api.z.ai/api/paas/v4/chat/completions', + apiKey: process.env.ZAI_API_KEY || '' + }) +} diff --git a/server/lib/chat/gpt.ts b/server/lib/chat/gpt.ts new file mode 100644 index 00000000..e8aeb1b9 --- /dev/null +++ b/server/lib/chat/gpt.ts @@ -0,0 +1,11 @@ +import { ChatRequest } from '../types' +import { streamOpenAICompatible } from './openaiCompatible' + +export function gpt(body: ChatRequest): Response { + return streamOpenAICompatible({ + body, + provider: 'openai', + apiUrl: 'https://api.openai.com/v1/chat/completions', + apiKey: process.env.OPENAI_API_KEY || '' + }) +} diff --git a/server/lib/chat/index.ts b/server/lib/chat/index.ts new file mode 100644 index 00000000..af3f2080 --- /dev/null +++ b/server/lib/chat/index.ts @@ -0,0 +1,16 @@ +import { ChatRequest } from '../types' +import { claude } from './claude' +import { gpt } from './gpt' +import { gemini } from './gemini' +import { glm } from './glm' +import { kimi } from './kimi' + +export type ChatHandler = (body: ChatRequest) => Response + +export const CHAT_HANDLERS: Record = { + claude, + gpt, + gemini, + glm, + kimi +} diff --git a/server/lib/chat/kimi.ts b/server/lib/chat/kimi.ts new file mode 100644 index 00000000..f051cb3e --- /dev/null +++ b/server/lib/chat/kimi.ts @@ -0,0 +1,11 @@ +import { ChatRequest } from '../types' +import { streamOpenAICompatible } from './openaiCompatible' + +export function kimi(body: ChatRequest): Response { + return streamOpenAICompatible({ + body, + provider: 'moonshot', + apiUrl: 'https://api.moonshot.ai/v1/chat/completions', + apiKey: process.env.MOONSHOT_API_KEY || '' + }) +} diff --git a/server/src/chat/openaiCompatible.ts b/server/lib/chat/openaiCompatible.ts similarity index 67% rename from server/src/chat/openaiCompatible.ts rename to server/lib/chat/openaiCompatible.ts index 4a0eaa05..af4a8945 100644 --- a/server/src/chat/openaiCompatible.ts +++ b/server/lib/chat/openaiCompatible.ts @@ -1,11 +1,9 @@ -import { Request, Response } from 'express' import { Provider, getChatModel } from '../models' import { ChatMessage, ChatRequest } from '../types' -import { initSSE, sendToken, sendError, sendDone, createSSEParser, pumpStream } from '../sse' +import { sseResponse, createSSEParser, pumpStream } from '../sse' interface StreamArgs { - req: Request - res: Response + body: ChatRequest provider: Provider apiUrl: string apiKey: string @@ -29,15 +27,13 @@ function toOpenAIMessages(messages: ChatMessage[], supportsVision: boolean) { }) } -export async function streamOpenAICompatible({ req, res, provider, apiUrl, apiKey }: StreamArgs) { - initSSE(res) - try { - const { model, messages }: ChatRequest = req.body +export function streamOpenAICompatible({ body, provider, apiUrl, apiKey }: StreamArgs): Response { + return sseResponse(async writer => { + const { model, messages } = body const chatModel = getChatModel(model) if (!chatModel || chatModel.provider !== provider) { - sendError(res, `unsupported model: ${model}`) - sendDone(res) + writer.sendError(`unsupported model: ${model}`) return } @@ -45,7 +41,7 @@ export async function streamOpenAICompatible({ req, res, provider, apiUrl, apiKe method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}` + Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ model: chatModel.modelId, @@ -57,8 +53,7 @@ export async function streamOpenAICompatible({ req, res, provider, apiUrl, apiKe if (!response.ok) { const detail = await response.text() console.error(`${provider} error:`, response.status, detail) - sendError(res, `provider error (${response.status})`) - sendDone(res) + writer.sendError(`provider error (${response.status})`) return } @@ -67,15 +62,10 @@ export async function streamOpenAICompatible({ req, res, provider, apiUrl, apiKe try { const parsed = JSON.parse(data) const content = parsed.choices?.[0]?.delta?.content - if (content) sendToken(res, content) + if (content) writer.sendToken(content) } catch {} }) await pumpStream(response.body, parse) - sendDone(res) - } catch (err) { - console.error(`error in ${provider} chat:`, err) - sendError(res, 'unexpected server error') - sendDone(res) - } + }) } diff --git a/server/src/images/gemini.ts b/server/lib/images/gemini.ts similarity index 63% rename from server/src/images/gemini.ts rename to server/lib/images/gemini.ts index ebaae0ff..00e5f8f1 100644 --- a/server/src/images/gemini.ts +++ b/server/lib/images/gemini.ts @@ -1,23 +1,30 @@ -import { Request, Response } from 'express' import { getImageModel } from '../models' const geminiApiBase = 'https://generativelanguage.googleapis.com/v1beta/models' +interface GeminiImageInput { + model: string + prompt?: string + file?: { + mimeType: string + data: string + } +} + function getInlineData(part: any) { return part?.inlineData || part?.inline_data } -export async function geminiImage(req: Request, res: Response) { +export async function geminiImage({ model, prompt, file }: GeminiImageInput): Promise { try { - const { prompt, model } = req.body const imageModel = getImageModel(model) if (!imageModel) { - return res.status(400).json({ error: `unsupported model: ${model}` }) + return Response.json({ error: `unsupported model: ${model}` }, { status: 400 }) } - if (!prompt && !req.file) { - return res.status(400).json({ error: 'a prompt or image is required' }) + if (!prompt && !file) { + return Response.json({ error: 'a prompt or image is required' }, { status: 400 }) } const parts: any[] = [] @@ -25,11 +32,11 @@ export async function geminiImage(req: Request, res: Response) { parts.push({ text: prompt }) } - if (req.file) { + if (file) { parts.push({ inline_data: { - mime_type: req.file.mimetype, - data: req.file.buffer.toString('base64') + mime_type: file.mimeType, + data: file.data } }) } @@ -51,7 +58,7 @@ export async function geminiImage(req: Request, res: Response) { if (!response.ok) { const detail = await response.text() console.error('gemini image error:', response.status, detail) - return res.status(502).json({ error: `provider error (${response.status})` }) + return Response.json({ error: `provider error (${response.status})` }, { status: 502 }) } const data = await response.json() @@ -60,19 +67,22 @@ export async function geminiImage(req: Request, res: Response) { const inlineData = getInlineData(imagePart) if (!inlineData?.data) { - return res.status(502).json({ - error: 'the model did not return an image', - details: data - }) + return Response.json( + { + error: 'the model did not return an image', + details: data + }, + { status: 502 } + ) } const mimeType = inlineData.mimeType || inlineData.mime_type || 'image/png' - return res.json({ + return Response.json({ image: `data:${mimeType};base64,${inlineData.data}` }) } catch (err) { console.error('error generating Gemini image:', err) - return res.status(500).json({ error: 'error generating image' }) + return Response.json({ error: 'error generating image' }, { status: 500 }) } } diff --git a/server/src/models.ts b/server/lib/models.ts similarity index 100% rename from server/src/models.ts rename to server/lib/models.ts diff --git a/server/lib/rateLimit.ts b/server/lib/rateLimit.ts new file mode 100644 index 00000000..20546e6f --- /dev/null +++ b/server/lib/rateLimit.ts @@ -0,0 +1,56 @@ +const WINDOW_MS = 60 * 1000 + +interface Bucket { + count: number + resetAt: number +} + +const buckets = new Map() + +function getLimit(): number { + return Number(process.env.RATE_LIMIT_PER_MINUTE || 60) +} + +function getClientKey(req: Request): string { + const forwarded = req.headers.get('x-forwarded-for') + if (forwarded) return forwarded.split(',')[0].trim() + return req.headers.get('x-real-ip') || 'unknown' +} + +/** + * Fixed-window in-memory rate limiter, keyed by client IP. Returns a 429 + * Response when the limit is exceeded, otherwise null. + */ +export function checkRateLimit(req: Request): Response | null { + const limit = getLimit() + const key = getClientKey(req) + const now = Date.now() + + let bucket = buckets.get(key) + if (!bucket || bucket.resetAt <= now) { + bucket = { count: 0, resetAt: now + WINDOW_MS } + buckets.set(key, bucket) + } + + bucket.count += 1 + + const remaining = Math.max(0, limit - bucket.count) + const resetSeconds = Math.ceil((bucket.resetAt - now) / 1000) + const headers = { + 'RateLimit-Limit': String(limit), + 'RateLimit-Remaining': String(remaining), + 'RateLimit-Reset': String(resetSeconds) + } + + if (bucket.count > limit) { + return Response.json( + { error: 'Too many requests, please try again later.' }, + { + status: 429, + headers: { ...headers, 'Retry-After': String(resetSeconds) } + } + ) + } + + return null +} diff --git a/server/lib/sse.ts b/server/lib/sse.ts new file mode 100644 index 00000000..33d4c549 --- /dev/null +++ b/server/lib/sse.ts @@ -0,0 +1,74 @@ +export interface SSEWriter { + sendToken: (content: string) => void + sendError: (message: string) => void +} + +/** + * Builds a streaming SSE Response. The provided `run` callback receives a + * writer for emitting token/error events; a terminating `[DONE]` event is + * always sent once it settles. + */ +export function sseResponse(run: (writer: SSEWriter) => Promise): Response { + const encoder = new TextEncoder() + const stream = new ReadableStream({ + async start(controller) { + const write = (chunk: string) => controller.enqueue(encoder.encode(chunk)) + const writer: SSEWriter = { + sendToken: content => write(`data: ${JSON.stringify({ content })}\n\n`), + sendError: message => write(`data: ${JSON.stringify({ error: message })}\n\n`) + } + try { + await run(writer) + } catch (err) { + console.error('error in sse stream:', err) + writer.sendError('unexpected server error') + } finally { + write('data: [DONE]\n\n') + controller.close() + } + } + }) + + return new Response(stream, { + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + Connection: 'keep-alive', + 'X-Accel-Buffering': 'no' + } + }) +} + +/** + * Incrementally parses an SSE byte stream, buffering partial lines across + * chunk boundaries. Calls onData for each complete `data:` payload. + */ +export function createSSEParser(onData: (data: string) => void) { + let buffer = '' + return (chunk: string) => { + buffer += chunk + const lines = buffer.split('\n') + buffer = lines.pop() || '' + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed.startsWith('data:')) continue + const data = trimmed.slice(5).trim() + if (data) onData(data) + } + } +} + +export async function pumpStream( + body: ReadableStream | null, + onChunk: (chunk: string) => void +) { + if (!body) return + const reader = body.getReader() + const decoder = new TextDecoder() + while (true) { + const { done, value } = await reader.read() + if (done) break + onChunk(decoder.decode(value, { stream: true })) + } +} diff --git a/server/src/types.ts b/server/lib/types.ts similarity index 100% rename from server/src/types.ts rename to server/lib/types.ts diff --git a/server/next.config.ts b/server/next.config.ts new file mode 100644 index 00000000..bdf4e84f --- /dev/null +++ b/server/next.config.ts @@ -0,0 +1,10 @@ +import type { NextConfig } from 'next' +import { fileURLToPath } from 'node:url' + +const nextConfig: NextConfig = { + turbopack: { + root: fileURLToPath(new URL('.', import.meta.url)) + } +} + +export default nextConfig diff --git a/server/package.json b/server/package.json index fc0c5085..17b86e34 100644 --- a/server/package.json +++ b/server/package.json @@ -1,39 +1,31 @@ { "name": "server", - "version": "1.1.0", - "description": "", - "main": "index.js", + "version": "2.0.0", + "private": true, + "description": "Next.js backend proxy for React Native AI", "scripts": { - "build": "npx tsc", - "start": "node dist/index.js", - "ngrok": "ngrok http 3050", - "dev": "concurrently \"npx tsc --watch\" \"nodemon --exitcrash dist/index.js\"", - "ngrokfixed": "ngrok http --domain=f82f850f17ee-443370837071186036.ngrok-free.app 3050", + "dev": "next dev -p 3050", + "build": "next build", + "start": "next start -p 3050", "typecheck": "tsc --noEmit", - "test": "vitest run" + "test": "vitest run", + "ngrok": "ngrok http 3050", + "ngrokfixed": "ngrok http --domain=f82f850f17ee-443370837071186036.ngrok-free.app 3050" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { - "@types/express": "^5.0.6", - "@types/node": "^26.1.0", - "cors": "^2.8.6", - "dotenv": "^17.4.2", - "express": "^5.2.1", - "express-async-handler": "^1.2.0", - "express-rate-limit": "^8.2.1", - "multer": "^2.2.0", - "typescript": "^5.9.3", + "next": "16.2.10", + "react": "19.2.3", + "react-dom": "19.2.3", "zod": "^3.25.76" }, "devDependencies": { - "@types/cors": "^2.8.19", - "@types/multer": "^2.2.0", - "@types/supertest": "^6.0.3", - "concurrently": "^10.0.3", - "nodemon": "^3.1.14", - "supertest": "^7.1.4", + "@types/node": "^24.10.1", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "typescript": "^5.9.3", "vitest": "^3.2.4" } } diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index 07479a14..ceb5a5aa 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -8,61 +8,40 @@ importers: .: dependencies: - '@types/express': - specifier: ^5.0.6 - version: 5.0.6 - '@types/node': - specifier: ^26.1.0 - version: 26.1.0 - cors: - specifier: ^2.8.6 - version: 2.8.6 - dotenv: - specifier: ^17.4.2 - version: 17.4.2 - express: - specifier: ^5.2.1 - version: 5.2.1 - express-async-handler: - specifier: ^1.2.0 - version: 1.2.0 - express-rate-limit: - specifier: ^8.2.1 - version: 8.5.2(express@5.2.1) - multer: - specifier: ^2.2.0 - version: 2.2.0 - typescript: - specifier: ^5.9.3 - version: 5.9.3 + next: + specifier: 16.2.10 + version: 16.2.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) zod: specifier: ^3.25.76 version: 3.25.76 devDependencies: - '@types/cors': - specifier: ^2.8.19 - version: 2.8.19 - '@types/multer': - specifier: ^2.2.0 - version: 2.2.0 - '@types/supertest': - specifier: ^6.0.3 - version: 6.0.3 - concurrently: - specifier: ^10.0.3 - version: 10.0.3 - nodemon: - specifier: ^3.1.14 - version: 3.1.14 - supertest: - specifier: ^7.1.4 - version: 7.2.2 + '@types/node': + specifier: ^24.10.1 + version: 24.13.3 + '@types/react': + specifier: ^19.2.17 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) + typescript: + specifier: ^5.9.3 + version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.7(@types/node@26.1.0) + version: 3.2.7(@types/node@24.13.3) packages: + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -219,15 +198,216 @@ packages: cpu: [x64] os: [win32] + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@noble/hashes@1.8.0': - resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} - engines: {node: ^14.21.3 || >=16} + '@next/env@16.2.10': + resolution: {integrity: sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==} + + '@next/swc-darwin-arm64@16.2.10': + resolution: {integrity: sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.2.10': + resolution: {integrity: sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.2.10': + resolution: {integrity: sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.2.10': + resolution: {integrity: sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.2.10': + resolution: {integrity: sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.2.10': + resolution: {integrity: sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.2.10': + resolution: {integrity: sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] - '@paralleldrive/cuid2@2.3.1': - resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@next/swc-win32-x64-msvc@16.2.10': + resolution: {integrity: sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] '@rollup/rollup-android-arm-eabi@4.62.2': resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} @@ -367,62 +547,28 @@ packages: cpu: [x64] os: [win32] - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@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.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@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/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - - '@types/methods@1.1.4': - resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} - - '@types/multer@2.2.0': - resolution: {integrity: sha512-3U1troeqGV8Ntp7Q3klwf4zr23VEoqYVocYXaswm9+8z3O9UHDYAqLxjJ/h550iRADTjKdOdhhasXw6gD6kYtg==} - - '@types/node@26.1.0': - resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==} - - '@types/qs@6.14.0': - resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} - - '@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.10': - resolution: {integrity: sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 - '@types/supertest@6.0.3': - resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} '@vitest/expect@3.2.7': resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} @@ -453,140 +599,35 @@ packages: '@vitest/utils@3.2.7': resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - append-field@1.0.0: - resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} - - asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - body-parser@2.3.0: - resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} - engines: {node: '>=18'} - - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - 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'} + baseline-browser-mapping@2.10.43: + resolution: {integrity: sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==} + engines: {node: '>=6.0.0'} + hasBin: true cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - call-bind-apply-helpers@1.0.2: - 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'} + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - cliui@9.0.1: - resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} - engines: {node: '>=20'} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - - component-emitter@1.3.1: - resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} - - concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} - - concurrently@10.0.3: - resolution: {integrity: sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==} - engines: {node: '>=22'} - hasBin: true - - content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} - engines: {node: '>=18'} - - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - cookiejar@2.1.4: - resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -601,93 +642,25 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - dezalgo@1.0.4: - resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} - - dotenv@17.4.2: - resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} - engines: {node: '>=12'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - emoji-regex@10.6.0: - resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-object-atoms@1.1.1: - 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'} - esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} - express-async-handler@1.2.0: - resolution: {integrity: sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==} - - express-rate-limit@8.5.2: - resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - - fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -697,385 +670,125 @@ packages: picomatch: optional: true - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - - formidable@3.5.4: - resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} - engines: {node: '>=14.0.0'} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@2.0.0: - 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} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - get-east-asian-width@1.6.0: - resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} - engines: {node: '>=18'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} + next@16.2.10: + resolution: {integrity: sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} + postcss@8.5.19: + resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} + engines: {node: ^10 || ^12 || >=14} - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 - iconv-lite@0.7.3: - resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} - ignore-by-default@1.0.1: - resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - ip-address@10.2.0: - resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} - engines: {node: '>= 12'} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - - methods@1.1.2: - 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'} - - mime-db@1.54.0: - 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'} - - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - - mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} - hasBin: true - - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - multer@2.2.0: - resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} - engines: {node: '>= 10.16.0'} - - nanoid@3.3.15: - resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} - 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'} - - nodemon@3.1.14: - resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} - engines: {node: '>=10'} - hasBin: true - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - object-assign@4.1.1: - 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'} - - on-finished@2.4.1: - 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==} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - path-to-regexp@8.4.2: - resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - - postcss@8.5.16: - resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} - engines: {node: ^10 || ^12 || >=14} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - pstree.remy@1.1.8: - resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} - - qs@6.14.1: - resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} - engines: {node: '>=0.6'} - - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} - engines: {node: '>=0.6'} - - range-parser@1.2.1: - 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'} - - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - rollup@4.62.2: - resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - shell-quote@1.8.4: - resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} - engines: {node: '>= 0.4'} - - side-channel-list@1.0.1: - 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'} - - side-channel-weakmap@1.0.2: - 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'} - - side-channel@1.1.1: - resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} - engines: {node: '>= 0.4'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - simple-update-notifier@2.0.0: - resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} - engines: {node: '>=10'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - - strip-ansi@7.2.0: - resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} - engines: {node: '>=12'} - - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - - superagent@10.3.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'} - - supports-color@10.2.2: - resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} - engines: {node: '>=18'} - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1099,57 +812,16 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - touch@3.1.1: - resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} - hasBin: true - - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - - typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true - undefsafe@2.0.5: - resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} @@ -1229,30 +901,16 @@ packages: engines: {node: '>=8'} hasBin: true - wrap-ansi@9.0.2: - resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} - engines: {node: '>=18'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yargs-parser@22.0.0: - resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=23} - - yargs@18.0.0: - resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=23} - zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} snapshots: + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.28.1': optional: true @@ -1331,13 +989,130 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@jridgewell/sourcemap-codec@1.5.5': {} + '@img/colour@1.1.0': + optional: true - '@noble/hashes@1.8.0': {} + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true - '@paralleldrive/cuid2@2.3.1': + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': dependencies: - '@noble/hashes': 1.8.0 + '@emnapi/runtime': 1.11.2 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@next/env@16.2.10': {} + + '@next/swc-darwin-arm64@16.2.10': + optional: true + + '@next/swc-darwin-x64@16.2.10': + optional: true + + '@next/swc-linux-arm64-gnu@16.2.10': + optional: true + + '@next/swc-linux-arm64-musl@16.2.10': + optional: true + + '@next/swc-linux-x64-gnu@16.2.10': + optional: true + + '@next/swc-linux-x64-musl@16.2.10': + optional: true + + '@next/swc-win32-arm64-msvc@16.2.10': + optional: true + + '@next/swc-win32-x64-msvc@16.2.10': + optional: true '@rollup/rollup-android-arm-eabi@4.62.2': optional: true @@ -1414,79 +1189,30 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@types/body-parser@1.19.6': + '@swc/helpers@0.5.15': dependencies: - '@types/connect': 3.4.38 - '@types/node': 26.1.0 + tslib: 2.8.1 '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/connect@3.4.38': - dependencies: - '@types/node': 26.1.0 - - '@types/cookiejar@2.1.5': {} - - '@types/cors@2.8.19': - dependencies: - '@types/node': 26.1.0 - '@types/deep-eql@4.0.2': {} '@types/estree@1.0.9': {} - '@types/express-serve-static-core@5.1.1': - dependencies: - '@types/node': 26.1.0 - '@types/qs': 6.14.0 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - - '@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/http-errors@2.0.5': {} - - '@types/methods@1.1.4': {} - - '@types/multer@2.2.0': + '@types/node@24.13.3': dependencies: - '@types/express': 5.0.6 + undici-types: 7.18.2 - '@types/node@26.1.0': + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: - undici-types: 8.3.0 + '@types/react': 19.2.17 - '@types/qs@6.14.0': {} - - '@types/range-parser@1.2.7': {} - - '@types/send@1.2.1': - dependencies: - '@types/node': 26.1.0 - - '@types/serve-static@2.2.0': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 26.1.0 - - '@types/superagent@8.1.10': - dependencies: - '@types/cookiejar': 2.1.5 - '@types/methods': 1.1.4 - '@types/node': 26.1.0 - form-data: 4.0.5 - - '@types/supertest@6.0.3': + '@types/react@19.2.17': dependencies: - '@types/methods': 1.1.4 - '@types/superagent': 8.1.10 + csstype: 3.2.3 '@vitest/expect@3.2.7': dependencies: @@ -1496,13 +1222,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@26.1.0))': + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@26.1.0) + vite: 7.3.6(@types/node@24.13.3) '@vitest/pretty-format@3.2.7': dependencies: @@ -1530,73 +1256,13 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - - ansi-regex@6.2.2: {} - - ansi-styles@6.2.3: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - append-field@1.0.0: {} - - asap@2.0.6: {} - assertion-error@2.0.1: {} - asynckit@0.4.0: {} - - balanced-match@4.0.4: {} - - binary-extensions@2.3.0: {} - - body-parser@2.3.0: - dependencies: - bytes: 3.1.2 - content-type: 2.0.0 - debug: 4.4.3(supports-color@5.5.0) - http-errors: 2.0.1 - iconv-lite: 0.7.3 - on-finished: 2.4.1 - qs: 6.15.3 - raw-body: 3.0.2 - type-is: 2.1.0 - transitivePeerDependencies: - - supports-color - - brace-expansion@5.0.7: - dependencies: - balanced-match: 4.0.4 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - buffer-from@1.1.2: {} - - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - - bytes@3.1.2: {} + baseline-browser-mapping@2.10.43: {} cac@6.7.14: {} - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 + caniuse-lite@1.0.30001806: {} chai@5.3.3: dependencies: @@ -1606,115 +1272,23 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 - chalk@5.6.2: {} - check-error@2.1.3: {} - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 + client-only@0.0.1: {} - cliui@9.0.1: - dependencies: - string-width: 7.2.0 - strip-ansi: 7.2.0 - wrap-ansi: 9.0.2 - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - - component-emitter@1.3.1: {} - - concat-stream@2.0.0: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - typedarray: 0.0.6 - - concurrently@10.0.3: - dependencies: - chalk: 5.6.2 - rxjs: 7.8.2 - shell-quote: 1.8.4 - supports-color: 10.2.2 - tree-kill: 1.2.2 - yargs: 18.0.0 + csstype@3.2.3: {} - content-disposition@1.1.0: {} - - content-type@1.0.5: {} - - content-type@2.0.0: {} - - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - - cookiejar@2.1.4: {} - - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - - debug@4.4.3(supports-color@5.5.0): + debug@4.4.3: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 5.5.0 deep-eql@5.0.2: {} - delayed-stream@1.0.0: {} - - depd@2.0.0: {} - - dezalgo@1.0.4: - dependencies: - asap: 2.0.6 - wrappy: 1.0.2 - - dotenv@17.4.2: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - ee-first@1.1.1: {} - - emoji-regex@10.6.0: {} - - encodeurl@2.0.0: {} - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} + detect-libc@2.1.2: + optional: true es-module-lexer@1.7.0: {} - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -1744,176 +1318,19 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 - escalade@3.2.0: {} - - escape-html@1.0.3: {} - estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 - etag@1.8.1: {} - expect-type@1.4.0: {} - express-async-handler@1.2.0: {} - - express-rate-limit@8.5.2(express@5.2.1): - dependencies: - express: 5.2.1 - ip-address: 10.2.0 - - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.3.0 - content-disposition: 1.1.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@5.5.0) - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.1 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.1.0 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - fast-safe-stringify@2.1.1: {} - fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - finalhandler@2.1.1: - dependencies: - debug: 4.4.3(supports-color@5.5.0) - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - - formidable@3.5.4: - dependencies: - '@paralleldrive/cuid2': 2.3.1 - dezalgo: 1.0.4 - once: 1.4.0 - - forwarded@0.2.0: {} - - fresh@2.0.0: {} - fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - - get-caller-file@2.0.5: {} - - get-east-asian-width@1.6.0: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - gopd@1.2.0: {} - - has-flag@3.0.0: {} - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - iconv-lite@0.7.3: - dependencies: - safer-buffer: 2.1.2 - - ignore-by-default@1.0.1: {} - - inherits@2.0.4: {} - - ip-address@10.2.0: {} - - ipaddr.js@1.9.1: {} - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-number@7.0.0: {} - - is-promise@4.0.0: {} - js-tokens@9.0.1: {} loupe@3.2.1: {} @@ -1922,77 +1339,33 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - math-intrinsics@1.1.0: {} - - media-typer@0.3.0: {} - - media-typer@1.1.0: {} - - merge-descriptors@2.0.0: {} - - methods@1.1.2: {} - - mime-db@1.52.0: {} - - mime-db@1.54.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - - mime@2.6.0: {} - - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.7 - ms@2.1.3: {} - multer@2.2.0: - dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 2.0.0 - type-is: 1.6.18 - - nanoid@3.3.15: {} + nanoid@3.3.16: {} - negotiator@1.0.0: {} - - nodemon@3.1.14: + next@16.2.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - chokidar: 3.6.0 - debug: 4.4.3(supports-color@5.5.0) - ignore-by-default: 1.0.1 - minimatch: 10.2.5 - pstree.remy: 1.1.8 - semver: 7.7.3 - simple-update-notifier: 2.0.0 - supports-color: 5.5.0 - touch: 3.1.1 - undefsafe: 2.0.5 - - normalize-path@3.0.0: {} - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - parseurl@1.3.3: {} - - path-to-regexp@8.4.2: {} + '@next/env': 16.2.10 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.43 + caniuse-lite: 1.0.30001806 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.2.10 + '@next/swc-darwin-x64': 16.2.10 + '@next/swc-linux-arm64-gnu': 16.2.10 + '@next/swc-linux-arm64-musl': 16.2.10 + '@next/swc-linux-x64-gnu': 16.2.10 + '@next/swc-linux-x64-musl': 16.2.10 + '@next/swc-win32-arm64-msvc': 16.2.10 + '@next/swc-win32-x64-msvc': 16.2.10 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros pathe@2.0.3: {} @@ -2000,50 +1373,26 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} - picomatch@4.0.5: {} - postcss@8.5.16: + postcss@8.4.31: dependencies: - nanoid: 3.3.15 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 - proxy-addr@2.0.7: + postcss@8.5.19: dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - pstree.remy@1.1.8: {} - - qs@6.14.1: - dependencies: - side-channel: 1.1.0 - - qs@6.15.3: - dependencies: - es-define-property: 1.0.1 - side-channel: 1.1.1 - - range-parser@1.2.1: {} - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.3 - unpipe: 1.0.0 + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 - readable-stream@3.6.2: + react-dom@19.2.3(react@19.2.3): dependencies: - inherits: 2.0.4 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 + react: 19.2.3 + scheduler: 0.27.0 - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 + react@19.2.3: {} rollup@4.62.2: dependencies: @@ -2076,152 +1425,59 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 - router@2.2.0: - dependencies: - debug: 4.4.3(supports-color@5.5.0) - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.4.2 - transitivePeerDependencies: - - supports-color - - rxjs@7.8.2: - dependencies: - tslib: 2.8.1 - - safe-buffer@5.1.2: {} - - safer-buffer@2.1.2: {} - - semver@7.7.3: {} + scheduler@0.27.0: {} - send@1.2.1: - dependencies: - debug: 4.4.3(supports-color@5.5.0) - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - - setprototypeof@1.2.0: {} - - shell-quote@1.8.4: {} - - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 + semver@7.8.5: + optional: true - side-channel@1.1.1: + sharp@0.34.5: dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true siginfo@2.0.0: {} - simple-update-notifier@2.0.0: - dependencies: - semver: 7.7.3 - source-map-js@1.2.1: {} stackback@0.0.2: {} - statuses@2.0.2: {} - std-env@3.10.0: {} - streamsearch@1.1.0: {} - - string-width@7.2.0: - dependencies: - emoji-regex: 10.6.0 - get-east-asian-width: 1.6.0 - strip-ansi: 7.2.0 - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - - strip-ansi@7.2.0: - dependencies: - ansi-regex: 6.2.2 - strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 - superagent@10.3.0: - dependencies: - component-emitter: 1.3.1 - cookiejar: 2.1.4 - debug: 4.4.3(supports-color@5.5.0) - fast-safe-stringify: 2.1.1 - form-data: 4.0.5 - formidable: 3.5.4 - methods: 1.1.2 - mime: 2.6.0 - qs: 6.15.3 - transitivePeerDependencies: - - supports-color - - supertest@7.2.2: - dependencies: - cookie-signature: 1.2.2 - methods: 1.1.2 - superagent: 10.3.0 - transitivePeerDependencies: - - supports-color - - supports-color@10.2.2: {} - - supports-color@5.5.0: + styled-jsx@5.1.6(react@19.2.3): dependencies: - has-flag: 3.0.0 + client-only: 0.0.1 + react: 19.2.3 tinybench@2.9.0: {} @@ -2238,50 +1494,19 @@ snapshots: tinyspy@4.0.4: {} - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - toidentifier@1.0.1: {} - - touch@3.1.1: {} - - tree-kill@1.2.2: {} - tslib@2.8.1: {} - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 - mime-types: 3.0.2 - - typedarray@0.0.6: {} - typescript@5.9.3: {} - undefsafe@2.0.5: {} - - undici-types@8.3.0: {} - - unpipe@1.0.0: {} - - util-deprecate@1.0.2: {} - - vary@1.1.2: {} + undici-types@7.18.2: {} - vite-node@3.2.4(@types/node@26.1.0): + vite-node@3.2.4(@types/node@24.13.3): dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6(@types/node@26.1.0) + vite: 7.3.6(@types/node@24.13.3) transitivePeerDependencies: - '@types/node' - jiti @@ -2296,30 +1521,30 @@ snapshots: - tsx - yaml - vite@7.3.6(@types/node@26.1.0): + vite@7.3.6(@types/node@24.13.3): dependencies: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 - postcss: 8.5.16 + postcss: 8.5.19 rollup: 4.62.2 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 24.13.3 fsevents: 2.3.3 - vitest@3.2.7(@types/node@26.1.0): + vitest@3.2.7(@types/node@24.13.3): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@26.1.0)) + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 '@vitest/spy': 3.2.7 '@vitest/utils': 3.2.7 chai: 5.3.3 - debug: 4.4.3(supports-color@5.5.0) + debug: 4.4.3 expect-type: 1.4.0 magic-string: 0.30.21 pathe: 2.0.3 @@ -2330,11 +1555,11 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.6(@types/node@26.1.0) - vite-node: 3.2.4(@types/node@26.1.0) + vite: 7.3.6(@types/node@24.13.3) + vite-node: 3.2.4(@types/node@24.13.3) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 24.13.3 transitivePeerDependencies: - jiti - less @@ -2354,25 +1579,4 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wrap-ansi@9.0.2: - dependencies: - ansi-styles: 6.2.3 - string-width: 7.2.0 - strip-ansi: 7.2.0 - - wrappy@1.0.2: {} - - y18n@5.0.8: {} - - yargs-parser@22.0.0: {} - - yargs@18.0.0: - dependencies: - cliui: 9.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - string-width: 7.2.0 - y18n: 5.0.8 - yargs-parser: 22.0.0 - zod@3.25.76: {} diff --git a/server/proxy.ts b/server/proxy.ts new file mode 100644 index 00000000..fc043bd4 --- /dev/null +++ b/server/proxy.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from 'next/server' + +function corsHeaders(): Record { + return { + 'Access-Control-Allow-Origin': process.env.CORS_ORIGIN || '*', + 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization' + } +} + +export function proxy(req: NextRequest) { + if (req.method === 'OPTIONS') { + return new NextResponse(null, { status: 204, headers: corsHeaders() }) + } + + const res = NextResponse.next() + for (const [key, value] of Object.entries(corsHeaders())) { + res.headers.set(key, value) + } + return res +} + +export const config = { + matcher: ['/health', '/models', '/chat/:path*', '/images/:path*'] +} diff --git a/server/src/__tests__/api.test.ts b/server/src/__tests__/api.test.ts deleted file mode 100644 index 95ed8f36..00000000 --- a/server/src/__tests__/api.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import request from 'supertest' -import { createApp } from '../index' - -function sseResponse(events: string[]) { - const encoder = new TextEncoder() - const stream = new ReadableStream({ - start(controller) { - for (const event of events) { - controller.enqueue(encoder.encode(event)) - } - controller.close() - } - }) - return new Response(stream, { - status: 200, - headers: { 'Content-Type': 'text/event-stream' } - }) -} - -describe('server', () => { - beforeEach(() => { - delete process.env.API_AUTH_TOKEN - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - it('GET /health returns ok', async () => { - const res = await request(createApp()).get('/health') - expect(res.status).toBe(200) - expect(res.body).toEqual({ status: 'ok' }) - }) - - it('GET /models returns chat and image models', async () => { - const res = await request(createApp()).get('/models') - expect(res.status).toBe(200) - expect(res.body.chatModels.length).toBeGreaterThan(0) - expect(res.body.imageModels.length).toBeGreaterThan(0) - const claude = res.body.chatModels.find((m: any) => m.label === 'claudeOpus') - expect(claude.provider).toBe('anthropic') - expect(res.body.chatModels.every((m: any) => !m.modelId)).toBe(true) - }) - - it('rejects invalid chat request bodies', async () => { - const res = await request(createApp()) - .post('/chat/claude') - .send({ model: 'claudeOpus' }) - expect(res.status).toBe(400) - expect(res.body.error).toBe('invalid request body') - }) - - it('requires bearer token when API_AUTH_TOKEN is set', async () => { - process.env.API_AUTH_TOKEN = 'secret' - const app = createApp() - const unauthorized = await request(app) - .post('/chat/claude') - .send({ model: 'claudeOpus', messages: [{ role: 'user', content: 'hi' }] }) - expect(unauthorized.status).toBe(401) - }) - - it('streams claude tokens as normalized SSE events', async () => { - vi.spyOn(global, 'fetch').mockResolvedValue( - sseResponse([ - 'data: {"type":"content_block_delta","delta":{"text":"Hel"}}\n\n', - 'data: {"type":"content_block_delta","del', - 'ta":{"text":"lo"}}\n\n' - ]) as any - ) - const res = await request(createApp()) - .post('/chat/claude') - .send({ model: 'claudeOpus', messages: [{ role: 'user', content: 'hi' }] }) - expect(res.status).toBe(200) - expect(res.text).toContain('data: {"content":"Hel"}') - expect(res.text).toContain('data: {"content":"lo"}') - expect(res.text).toContain('data: [DONE]') - }) - - it('streams openai-compatible tokens as normalized SSE events', async () => { - vi.spyOn(global, 'fetch').mockResolvedValue( - sseResponse([ - 'data: {"choices":[{"delta":{"content":"Hi"}}]}\n\n', - 'data: [DONE]\n\n' - ]) as any - ) - const res = await request(createApp()) - .post('/chat/gpt') - .send({ model: 'gpt52', messages: [{ role: 'user', content: 'hi' }] }) - expect(res.status).toBe(200) - expect(res.text).toContain('data: {"content":"Hi"}') - expect(res.text).toContain('data: [DONE]') - }) - - it('emits an error event for unsupported models', async () => { - const res = await request(createApp()) - .post('/chat/gpt') - .send({ model: 'not-a-model', messages: [{ role: 'user', content: 'hi' }] }) - expect(res.text).toContain('unsupported model') - expect(res.text).toContain('data: [DONE]') - }) - - it('emits an error event when the provider fails', async () => { - vi.spyOn(global, 'fetch').mockResolvedValue( - new Response('rate limited', { status: 429 }) as any - ) - const res = await request(createApp()) - .post('/chat/claude') - .send({ model: 'claudeOpus', messages: [{ role: 'user', content: 'hi' }] }) - expect(res.text).toContain('provider error (429)') - expect(res.text).toContain('data: [DONE]') - }) -}) diff --git a/server/src/chat/chatRouter.ts b/server/src/chat/chatRouter.ts deleted file mode 100644 index 50faaeb1..00000000 --- a/server/src/chat/chatRouter.ts +++ /dev/null @@ -1,20 +0,0 @@ -import express from 'express' -import { claude } from './claude' -import { gpt } from './gpt' -import { gemini } from './gemini' -import { glm } from './glm' -import { kimi } from './kimi' -import { validateBody } from '../middleware' -import { chatRequestSchema } from '../types' - -const router = express.Router() - -router.use(validateBody(chatRequestSchema)) - -router.post('/claude', claude) -router.post('/gpt', gpt) -router.post('/gemini', gemini) -router.post('/glm', glm) -router.post('/kimi', kimi) - -export default router diff --git a/server/src/chat/glm.ts b/server/src/chat/glm.ts deleted file mode 100644 index bfe0c81e..00000000 --- a/server/src/chat/glm.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Request, Response } from 'express' -import asyncHandler from 'express-async-handler' -import { streamOpenAICompatible } from './openaiCompatible' - -export const glm = asyncHandler(async (req: Request, res: Response) => { - await streamOpenAICompatible({ - req, - res, - provider: 'zai', - apiUrl: 'https://api.z.ai/api/paas/v4/chat/completions', - apiKey: process.env.ZAI_API_KEY || '' - }) -}) diff --git a/server/src/chat/gpt.ts b/server/src/chat/gpt.ts deleted file mode 100644 index c8860e1c..00000000 --- a/server/src/chat/gpt.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Request, Response } from 'express' -import asyncHandler from 'express-async-handler' -import { streamOpenAICompatible } from './openaiCompatible' - -export const gpt = asyncHandler(async (req: Request, res: Response) => { - await streamOpenAICompatible({ - req, - res, - provider: 'openai', - apiUrl: 'https://api.openai.com/v1/chat/completions', - apiKey: process.env.OPENAI_API_KEY || '' - }) -}) diff --git a/server/src/chat/kimi.ts b/server/src/chat/kimi.ts deleted file mode 100644 index 6970b874..00000000 --- a/server/src/chat/kimi.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Request, Response } from 'express' -import asyncHandler from 'express-async-handler' -import { streamOpenAICompatible } from './openaiCompatible' - -export const kimi = asyncHandler(async (req: Request, res: Response) => { - await streamOpenAICompatible({ - req, - res, - provider: 'moonshot', - apiUrl: 'https://api.moonshot.ai/v1/chat/completions', - apiKey: process.env.MOONSHOT_API_KEY || '' - }) -}) diff --git a/server/src/images/imagesRouter.ts b/server/src/images/imagesRouter.ts deleted file mode 100644 index 158fe4e2..00000000 --- a/server/src/images/imagesRouter.ts +++ /dev/null @@ -1,10 +0,0 @@ -import express from 'express' -import multer from 'multer' -import { geminiImage } from './gemini' - -const upload = multer() -const router = express.Router() - -router.post('/gemini', upload.single('file'), geminiImage) - -export default router diff --git a/server/src/index.ts b/server/src/index.ts deleted file mode 100644 index 6d6c62d5..00000000 --- a/server/src/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import express from 'express' -import cors from 'cors' -import rateLimit from 'express-rate-limit' -import chatRouter from './chat/chatRouter' -import imagesRouter from './images/imagesRouter' -import { auth } from './middleware' -import { CHAT_MODELS, IMAGE_MODELS } from './models' -import 'dotenv/config' - -export function createApp() { - const app = express() - - app.use(cors({ origin: process.env.CORS_ORIGIN || '*' })) - app.use(express.json({ limit: '50mb' })) - app.use(express.urlencoded({ extended: true })) - - app.get('/health', (req, res) => { - res.json({ status: 'ok' }) - }) - - app.get('/models', (req, res) => { - res.json({ - chatModels: Object.values(CHAT_MODELS).map(({ name, label, provider, supportsVision }) => ({ - name, label, provider, supportsVision - })), - imageModels: Object.values(IMAGE_MODELS).map(({ name, label, provider }) => ({ - name, label, provider - })) - }) - }) - - const limiter = rateLimit({ - windowMs: 60 * 1000, - limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 60), - standardHeaders: true, - legacyHeaders: false - }) - - app.use('/chat', auth, limiter, chatRouter) - app.use('/images', auth, limiter, imagesRouter) - - return app -} - -if (require.main === module) { - const port = Number(process.env.PORT || 3050) - createApp().listen(port, () => { - console.log(`Server started on port ${port}`) - }) -} diff --git a/server/src/middleware.ts b/server/src/middleware.ts deleted file mode 100644 index 4b7f136d..00000000 --- a/server/src/middleware.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Request, Response, NextFunction } from 'express' -import { timingSafeEqual } from 'crypto' -import { ZodSchema } from 'zod' - -/** - * Optional bearer-token auth. Enabled by setting API_AUTH_TOKEN in the - * server environment; clients must then send `Authorization: Bearer `. - */ -export function auth(req: Request, res: Response, next: NextFunction) { - const token = process.env.API_AUTH_TOKEN - if (!token) return next() - const header = req.headers.authorization || '' - const expected = Buffer.from(`Bearer ${token}`) - const provided = Buffer.from(header) - if ( - expected.length === provided.length && - timingSafeEqual(expected, provided) - ) return next() - res.status(401).json({ error: 'unauthorized' }) -} - -export function validateBody(schema: ZodSchema) { - return (req: Request, res: Response, next: NextFunction) => { - const result = schema.safeParse(req.body) - if (!result.success) { - res.status(400).json({ - error: 'invalid request body', - details: result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`) - }) - return - } - req.body = result.data - next() - } -} diff --git a/server/src/sse.ts b/server/src/sse.ts deleted file mode 100644 index b2fc5f77..00000000 --- a/server/src/sse.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Response } from 'express' - -export function initSSE(res: Response) { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Connection': 'keep-alive', - 'Cache-Control': 'no-cache' - }) -} - -export function sendToken(res: Response, content: string) { - res.write(`data: ${JSON.stringify({ content })}\n\n`) -} - -export function sendError(res: Response, message: string) { - res.write(`data: ${JSON.stringify({ error: message })}\n\n`) -} - -export function sendDone(res: Response) { - res.write('data: [DONE]\n\n') - res.end() -} - -/** - * Incrementally parses an SSE byte stream, buffering partial lines across - * chunk boundaries. Calls onData for each complete `data:` payload. - */ -export function createSSEParser(onData: (data: string) => void) { - let buffer = '' - return (chunk: string) => { - buffer += chunk - const lines = buffer.split('\n') - buffer = lines.pop() || '' - for (const line of lines) { - const trimmed = line.trim() - if (!trimmed.startsWith('data:')) continue - const data = trimmed.slice(5).trim() - if (data) onData(data) - } - } -} - -export async function pumpStream( - body: ReadableStream | null, - onChunk: (chunk: string) => void -) { - if (!body) return - const reader = body.getReader() - const decoder = new TextDecoder() - while (true) { - const { done, value } = await reader.read() - if (done) break - onChunk(decoder.decode(value, { stream: true })) - } -} diff --git a/server/tsconfig.json b/server/tsconfig.json index 24dc737c..2478e5bf 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,109 +1,42 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "CommonJS", /* Specify what module code is generated. */ - "rootDir": "./src", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "forceConsistentCasingInFileNames": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/server/vitest.config.ts b/server/vitest.config.ts new file mode 100644 index 00000000..f0f42a10 --- /dev/null +++ b/server/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' +import { fileURLToPath } from 'node:url' + +export default defineConfig({ + resolve: { + alias: { + '@': fileURLToPath(new URL('.', import.meta.url)) + } + }, + test: { + environment: 'node', + include: ['__tests__/**/*.test.ts'] + } +}) From 168e091bb732dcde3a88c49bbd0c72ecbda8626b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:32:51 +0000 Subject: [PATCH 2/2] Evict expired rate-limit buckets to prevent unbounded memory growth Co-Authored-By: Nader Dabit --- server/lib/rateLimit.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/server/lib/rateLimit.ts b/server/lib/rateLimit.ts index 20546e6f..b51d50e5 100644 --- a/server/lib/rateLimit.ts +++ b/server/lib/rateLimit.ts @@ -6,11 +6,24 @@ interface Bucket { } const buckets = new Map() +let lastSweep = Date.now() function getLimit(): number { return Number(process.env.RATE_LIMIT_PER_MINUTE || 60) } +/** + * Amortized cleanup: at most once per window, drop buckets whose window has + * fully elapsed so keys from one-time clients don't accumulate forever. + */ +function sweepExpired(now: number) { + if (now - lastSweep < WINDOW_MS) return + lastSweep = now + for (const [key, bucket] of buckets) { + if (bucket.resetAt <= now) buckets.delete(key) + } +} + function getClientKey(req: Request): string { const forwarded = req.headers.get('x-forwarded-for') if (forwarded) return forwarded.split(',')[0].trim() @@ -26,6 +39,8 @@ export function checkRateLimit(req: Request): Response | null { const key = getClientKey(req) const now = Date.now() + sweepExpired(now) + let bucket = buckets.get(key) if (!bucket || bucket.resetAt <= now) { bucket = { count: 0, resetAt: now + WINDOW_MS }